1. 一个购物车,撑起了半条交易链路
软件测试这个行当里,面试官最爱问的一类题就是“给你一个功能,你怎么设计测试用例”。购物车是这道题里的常客,因为它太日常了,日常到很多人觉得它没什么可测的。可真把购物车当成一个被测系统摊开来看,你会发现它是整个交易链路里信息密度最高的一个页面:商品、库存、价格、优惠、会员、风控、多端同步,全在这里交汇。而在所有购物车相关的问题里,“为什么购物车要设置宝贝上限”这个问题,几乎每次都能把人问住,因为大部分人只看到“加不进去”这个表象,没看到背后那本账。
这篇内容我打算把两件事讲透:一是购物车该怎么系统性地设计测试用例,包括怎么划分功能域、怎么处理组合爆炸、怎么设计边界和并发场景;二是宝贝上限这件事到底为什么存在,从存储、查询、计算、风控到用户体验,一层一层拆给你看,并且给出上限值该怎么估算、专项用例该怎么写。不管你是刚入行的测试,还是已经写了两年代码想补一补用例设计思路的开发,这篇都能直接拿走用。
先说清楚一个容易被混用的概念。购物车里的“上限”其实是三种不同的东西:宝贝种类上限,也就是最多能放多少种不同的商品,常见值是 100 到 200;单个宝贝数量上限,同一种商品最多能加多少件,常见是 999;限购数量,这是营销规则,某些活动商品每人限买 2 件。这三者的触发条件、提示文案、校验位置完全不一样,测试用例也必须分开写。下面讲的上限,如果没有特别说明,都指第一种,也就是种类上限。
2. 先把被测对象拆干净:购物车的功能域地图
2.1 五个功能域,一个都不能漏
很多人写购物车用例,写着写着就变成了“加购、删除、结算”三条,然后就没词了。问题出在没有先把被测对象拆开。我的习惯是按数据流向拆成五个域:入口域、列表域、数量域、价格域、结算域。
入口域管的是“商品怎么进到购物车里”,包括商品详情页加购、列表页快速加购、活动页批量加购、分享链接打开后加购、扫码加购、以及登录后本地购物车与服务端购物车的合并。列表域管的是“购物车长什么样”,包括店铺分组、全选与单选、失效商品区、降价提醒、凑单推荐、排序规则。数量域管加减、手动输入、长按连加、库存校验。价格域管单价展示、活动价、会员价、优惠预估、满减提示。结算域管选中商品跳转确认订单页时传递的参数是否准确。
这么拆的好处是,每个域内部的用例可以穷举,域与域之间的交互单独列一层。我见过太多用例漏掉“登录后合并购物车”这种场景,结果上线后用户反馈“我加的东西没了”,一查是合并逻辑里同商品没有做数量累加,直接覆盖了。
2.2 状态机思维:购物车里的东西是会“变质”的
购物车最麻烦的地方在于,里面的商品不是静态数据,它会随时间变化。今天加进去的商品,明天可能下架了、涨价了、库存没了、活动结束了。所以设计用例时,一定要用状态迁移的思路,把商品在购物车里的生命周期画出来。
| 状态 | 触发条件 | 用户可见表现 | 关键校验点 |
|---|---|---|---|
| 正常 | 加购成功,商品在售有库存 | 可勾选、可改数量、可结算 | 价格与详情页一致 |
| 降价 | 商品价格低于加购时价格 | 显示降价标签 | 标签计算口径、是否可关闭 |
| 涨价 | 价格高于加购时价格 | 部分平台会提示 | 提示文案与实际差价一致 |
| 库存不足 | 库存小于购物车数量 | 数量置灰或提示 | 改数量时的上限跟随库存 |
| 售罄 | 库存为 0 | 移入失效区、不可勾选 | 全选时是否被排除 |
| 下架/删除 | 商品被商家下架 | 移入失效区 | 是否计入种类上限 |
| 活动失效 | 限时活动的价格到期 | 恢复原价 | 预估优惠是否同步刷新 |
| 预售 | 商品转为预售模式 | 显示定金/尾款 | 定金计算与尾款时间 |
这张表的价值在于,每一行都能横向展开出一组用例。比如“售罄”这一行,至少要测:售罄商品是否自动取消勾选、全选按钮是否会把它算进去、结算时是否被过滤、售罄后重新补货能否恢复为正常状态、失效区商品的删除是否影响其他商品、失效商品是否占用种类上限。这些细节单看都很小,但每一个都真实地被用户投诉过。
2.3 用户角色维度:登录态差异是重灾区
还有一个维度经常被忽略:用户角色。未登录用户、登录用户、多端同时登录的用户,购物车的行为完全不同。
未登录时购物车一般存在本地,容量有限,而且换设备就没了;登录后要做本地与服务端的合并。合并规则就是一堆坑:同商品同规格要不要累加数量?累加后超过单个宝贝上限怎么办?超过种类上限怎么办?本地商品的加购时间和服务端冲突时以谁为准?本地有但服务端已下架的商品怎么处理?这些问题如果没有明确的产品规则,测试就只能靠猜,最后上线全靠运气。
我的建议是,测试在写用例之前,先把合并规则用一张表跟产品对齐,明确“谁优先、超限怎么截断、冲突怎么提示”。这张表本身就是最好的用例来源。
3. 用例设计的六种武器,别只会等价类
3.1 从等价类到正交实验,按场景选方法
用例设计方法不用贪多,但要会用在对的地方。等价类划分适合处理输入域的无限性,比如手动输入数量,有效等价类是 1 到库存上限的整数,无效等价类是 0、负数、小数、非数字、超大数、空值、全角数字、带空格、带前导零。边界值适合处理阈值,数量为 1、库存上限、库存上限加一,这三种是最容易出 bug 的。判定表适合处理多条件组合,比如“是否会员 × 是否参与活动 × 是否使用优惠券 × 是否满减”,四个条件就是 16 条规则,用判定表列出来一目了然。
状态迁移前面讲过了,处理商品生命周期。场景法处理端到端的用户路径,比如“加购 → 改数量 → 部分勾选 → 结算 → 返回购物车 → 删除”。正交实验法专门对付组合爆炸,价格和优惠那块最需要它。
举个实际的例子。一个商品在购物车里的价格受这些因素影响:会员等级(3 种)、活动类型(5 种)、优惠券(4 种)、购买数量档位(3 档)。全排列是 180 种组合,全测一遍不现实。用正交表可以压到 16 组左右,覆盖所有两两组合,性价比极高。我在实际项目里用这个方法把价格域的用例从两百多条压到三十条,缺陷发现率基本没降。
3.2 购物车核心用例矩阵
下面这张表是我常用的骨架,可以直接拿去改。每一行还能往下拆,但先用这个保证不漏大块。
| 功能域 | 典型用例 | 预期结果 | 优先级 |
|---|---|---|---|
| 加购 | 详情页选择规格后加购 | 加购成功,角标数量加一 | P0 |
| 加购 | 未选规格直接点加购 | 弹出规格选择,不写入购物车 | P0 |
| 加购 | 重复加购同一商品同一规格 | 数量累加,不新增一行 | P0 |
| 加购 | 加购已售罄商品 | 按钮置灰或提示,不写入 | P1 |
| 加购 | 弱网下连续点击加购三次 | 最终数量加三,无重复行无丢失 | P1 |
| 列表 | 购物车为空 | 展示空态引导,不报错 | P2 |
| 列表 | 多个店铺商品混合展示 | 按店铺分组,小计正确 | P1 |
| 列表 | 全选/取消全选 | 所有可勾选商品同步,失效商品不参与 | P0 |
| 列表 | 只勾选部分商品 | 合计金额只统计勾选项 | P0 |
| 数量 | 点加号到库存上限 | 按钮置灰,提示库存不足 | P0 |
| 数量 | 点减号到 1 再点减号 | 不删除或弹确认删除,行为需明确 | P1 |
| 数量 | 手动输入 0 / 负数 / 字母 | 拦截并回填原值 | P0 |
| 数量 | 手动输入超过库存的数 | 截断到库存上限并提示 | P0 |
| 数量 | 长按加号连续增加 | 数字平滑递增,不卡死不跳变 | P2 |
| 价格 | 会员价与非会员价展示 | 与详情页一致,合计按会员价算 | P0 |
| 价格 | 满减门槛差一元 | 提示凑单,且凑单后金额正确 | P1 |
| 价格 | 优惠券即将过期 | 展示过期提醒,过期后自动取消 | P2 |
| 结算 | 勾选商品后点结算 | 跳转确认页,商品、数量、价格完全一致 | P0 |
| 结算 | 未勾选任何商品点结算 | 按钮置灰或提示先选择商品 | P0 |
| 结算 | 结算过程中商品被下架 | 确认页提示失效,金额重新计算 | P1 |
| 同步 | A 端加购,B 端刷新 | B 端能看到新增商品 | P1 |
| 同步 | 两端同时修改同一商品数量 | 以最后提交为准或以服务端为准,需明确 | P1 |
3.3 数量与库存的联动,最容易埋雷的地方
数量输入框是购物车里 bug 密度最高的控件,没有之一。因为它同时受三个约束:最小值 1、库存上限、单个宝贝数量上限。三个约束叠加,边界就变得很微妙。
假设某商品库存 50,单宝贝数量上限 999。用户输入 60,应该被截断到 50 并提示库存不足。用户输入 999,同样截断到 50。但如果商品是预售,库存为虚拟值,那上限又要按预售规则算。如果用户先输入 50,然后库存被其他用户买走变成 30,此时购物车该怎么做?常见做法是刷新时把数量改成 30 并提示,而不是直接判定失效。
注意:手动输入框一定要测“全角数字”和“带空格的复制粘贴”。这两个场景是真实用户会触发的,尤其是从聊天软件复制地址时经常带不可见字符,前端如果只用 Number() 转换,很容易转出 NaN 然后整个页面白屏。
还有一个高频问题:点减号减到 1 之后再点一次,是删除还是不动?不同平台策略不同,有的弹二次确认,有的直接删除,有的置灰。这里没有对错,但必须有明确规则,且全端一致。我遇到过 App 端是弹确认、H5 端是直接删除,用户在两端切换时心态直接崩了。
3.4 组合爆炸不是靠加班解决的
前面提到了价格域的 180 种组合。除了正交实验,还有两个实用技巧。
第一个是分层测试。把价格计算拆成“单品价格计算”和“整单优惠计算”两层,单品层用枚举,整单层用正交。单品层的组合其实不多,就是基础价、活动价、会员价三者的优先级问题,写一张判定表就够。整单层才是组合爆炸的来源,因为优惠规则会互相影响。
第二个是规则引擎的等价类化简。满 100 减 10、满 200 减 30 这类阶梯满减,本质上只需要测三类金额:刚好差一元到门槛、刚好等于门槛、远超门槛。如果满减还能叠加优惠券,那就在这三个位置各测“有券”和“无券”两种情况,九个用例就能覆盖住主要风险。
我在一个模拟项目里做过对比:不做化简的用例集有 240 条,执行一轮要 6 小时;化简后 42 条,执行 50 分钟,连续三个版本的全量回归里,两者发现的缺陷数都是 5 个,且高度重合。这就是方法的价值。
3.5 弱网、并发、多端:真实用户不会在理想环境里点按钮
测试环境永远网络良好,真实用户永远在地铁里。所以弱网场景必须单独设计。
| 场景 | 构造方式 | 关注点 |
|---|---|---|
| 加购请求超时 | 限制上传带宽或注入延迟 | 前端是否有防重复提交,是否提示失败 |
| 加购响应丢失 | 拦截响应不回包 | 用户重试后是否产生重复行 |
| 结算请求超时后重试 | 延迟响应 | 是否生成重复订单(幂等性) |
| 快速连点加购 | 脚本模拟 10 次点击 | 最终数量是否为 10,是否有重复行 |
| 库存临界并发 | 两人同时抢最后一件 | 只有一人成功,另一人收到明确提示 |
| 上下架与加购并发 | 加购瞬间商品下架 | 加购失败并提示,不产生脏数据 |
并发这块,最典型的 bug 是“超卖”。购物车本身不扣库存,但加购时通常会做库存校验,如果校验和写入之间有间隙,就会出现购物车里有 5 件、实际库存只有 3 件的情况。这种问题在测试环境很难复现,需要靠压测工具或者人为加延迟来放大时间窗口。
4. 宝贝上限:为什么平台不肯让你放无限多
现在进入正题的下半部分。用户加购到第 201 件商品,页面弹出“购物车已满,请先清理”,第一反应一定是“这破平台,连个购物车都要限制我”。但从系统设计的角度看,这个限制几乎是必然的。下面从技术、业务、体验三个层面把账算给你看。
4.1 技术账本:每一条记录都在消耗真金白银
先算存储。一条购物车记录,抛开业务扩展字段,核心字段大概是:用户 ID、商品 ID、规格 ID、数量、加购时间、来源渠道、勾选状态,再加上若干业务标记。原始数据估下来 60 到 80 字节,但加上序列化开销、索引开销、副本,工程上一般按 300 到 500 字节估算比较稳妥。
假设一个平台的日活是一亿,人均购物车里躺 20 件商品,那就是 20 亿行数据。按每行 400 字节算,原始数据接近 80GB,算上三副本和索引,实际占用在 300GB 量级。这个规模本身没问题,分布式存储扛得住。真正的麻烦在尾部。
没有上限的情况下,一定会出现“购物车囤积型”用户。我见过真实数据里,有用户的购物车塞了几千件商品,纯粹当收藏夹用。这类用户虽然占比极低,但他们把 P99 甚至 P999 的指标拉得非常难看。而系统容量规划看的就是分位数,一旦 P99 从 130 涨到 400,意味着缓存要扩容、查询要优化、成本直接上去。
再看缓存。购物车这类高频读写的数据,基本都会放在内存缓存里,常见结构是哈希,键是用户 ID,字段是规格 ID。哈希结构虽然没有硬性的字段数量限制,但工程上有一条经验红线:单个键的字段数控制在五千以内,总大小控制在十兆以内。原因是当这个键需要整体读取或删除时,操作是阻塞的,字段越多阻塞时间越长。
按 200 件上限算,一个用户的购物车数据大约是 200 乘以 200 字节,也就是 40KB 左右,非常安全。如果不设上限,一个几千件的购物车就是几百 KB,在内存里属于典型的大键,一旦这类键多了,内存碎片和阻塞风险都会上升。
还有分片倾斜的问题。购物车通常按用户 ID 哈希分片,一个囤了五千件商品的用户,他所在的分片承载的数据量是平均值的几十倍。如果这类用户恰好集中在少数分片,就会出现明显的数据倾斜,扩容也解决不了。设上限,本质上是在源头上掐断这种长尾。
4.2 计算账本:结算页的复杂度是乘法不是加法
比存储更贵的是计算。购物车页要展示预估优惠,结算页要精确计算每一分钱,这两处都需要遍历所有勾选商品。
单品层的计算是线性的,N 件商品就是 N 次价格查询。但整单优惠不是。满减、优惠券、会员折扣、店铺券、平台券,这些规则往往需要组合判断,复杂度接近 N 乘以 M,M 是规则数量。如果 N 是 1000,M 是 50,那就是五万次判断,单次请求的 CPU 时间可能从几毫秒涨到几十毫秒。对于一个每天处理上亿次请求的页面,这个差异就是几千核 CPU 的差距。
还有优惠分摊。一张满 300 减 50 的券,需要按商品金额比例分摊到每一件商品上,这样退款时才能精确退。分摊算法本身是 O(N),而且涉及浮点或高精度运算,N 越大,精度处理的成本和出错概率都越高。
从这个角度看,200 件上限其实是一个“计算预算”。它保证了最坏情况下的计算量在一个可控区间内,让 P99 响应时间能够稳定在可接受范围。
4.3 业务账本:上限也是一道风控闸门
库存是商家的稀缺资源。有人批量加购、批量占位,虽然下单才会真正扣减库存,但很多平台在加购环节会做库存预占或软占用,目的是防止用户结算时才发现没货。这就给了薅羊毛和恶意竞争可乘之机:把竞品的热门商品全部加购占住,让真实用户加不进去。
种类上限在这里起到了天然的限流作用。一个账号最多占 200 个坑位,想再多就得换账号,成本立刻上升,而且换账号的行为在风控系统里是很明显的特征。所以这道限制不只是技术妥协,它同时是一道业务防线。
另外还有一层:凑单和比价带来的无效数据。用户为了凑满减,会临时加一堆不打算买的商品,结算完再删。如果没有上限,这种行为会让购物车数据量出现周期性的尖峰,大促期间尤其明显。
4.4 上限值到底怎么定:用量化方法代替拍脑袋
“200”这个数字不是随便拍出来的。合理的做法是基于真实分布取分位数,再留余量。
具体步骤是这样的:拉取近 90 天的购物车快照,统计每个用户的购物车商品件数,算出 P50、P90、P99、P99.9。假设结果是这样:P50 是 8 件,P90 是 35 件,P99 是 120 件,P99.9 是 260 件。那么 200 这个值就非常合理,它覆盖了 99% 以上的用户,同时把极端长尾挡在门外。如果直接取 P99.9 的 260,成本上升但用户体验提升极其有限,因为受影响的那 0.1% 用户里,很大一部分本来就是当收藏夹用的。
除了分位数,还要看一个业务指标:加购后 7 天内下单的商品件数分布。如果一个用户购物车里有 150 件商品,但 7 天内真正下单的只有 5 件,说明大部分是囤积,这部分数据的价值很低,不值得为它付出存储和计算成本。
提示:上限值确定后,一定要做灰度。先对 10% 用户放开到 300,观察购物车页面的 P99 响应时间、缓存内存占用、结算转化率三个指标,如果响应时间和内存都没明显变化,转化率也没提升,那就说明这个上限不是瓶颈,维持原值即可。
5. 上限功能的专项测试用例怎么写
5.1 边界值是核心,但别只测边界
假设种类上限是 200。最直接的用例当然是 199、200、201 三个点,但真正容易出 bug 的是“边界加状态”的组合。
| 场景 | 前置状态 | 操作 | 预期结果 |
|---|---|---|---|
| 未达上限 | 已有 199 种 | 加购 1 种新商品 | 成功,变为 200 种 |
| 刚好达上限 | 已有 199 种 | 加购 1 种,实际写入 2 次 | 只成功 1 次,第 2 次拦截 |
| 已达上限 | 已有 200 种 | 加购 1 种新商品 | 拦截,提示清理购物车 |
| 已达上限但加已有商品 | 已有 200 种,其中含 A | 再次加购 A | 成功,A 数量加 1,种类仍 200 |
| 已达上限含失效商品 | 200 种中有 5 种已失效 | 加购新商品 | 需明确失效商品是否占用名额 |
| 达上限后删除再加 | 已有 200 种 | 删 1 种再加 1 种 | 成功,变为 200 种 |
| 达上限后移入收藏 | 已有 200 种 | 移入收藏再清空 | 名额是否释放 |
| 上限配置变更 | 原上限 200 改为 100 | 用户已有 150 种,访问购物车 | 不删除数据,只禁止新增 |
| 并发加购 | 已有 199 种 | 两端同时加购不同商品 | 只有一个成功,另一个提示已满 |
| 合并购物车超限 | 本地 100 种,服务端 150 种 | 登录触发合并 | 按规则截断并明确提示 |
这十行里,我认为最容易漏的是第五行和第八行。失效商品到底占不占名额,这在不同平台的做法不一样:有的平台认为失效商品不占名额,用户可以无限堆积失效商品;有的平台认为占名额,因为数据行确实存在。这两种做法各有道理,但必须写清楚,并且测试要覆盖,否则用户会遇到“我明明只有 180 件有效商品,为什么说满了”的困惑。
第八行是配置变更。上限从 200 调到 100,那已经超出的用户怎么办?正确做法是存量数据不动,只是禁止新增,并且允许用户删到 100 以下。如果实现时直接做了一次清理任务,把超出的商品删掉,那就是严重的数据事故,用户会直接投诉。这个场景必须测。
5.2 提示文案和引导路径,也是功能的一部分
上限拦截不只是返回一个错误码,它还包含一整套交互:提示文案、清理入口、失效商品一键清理、跳转到收藏夹。这些都需要测。
提示文案要测两点:一是准确性,要明确告诉用户“购物车最多放 200 种商品”,而不是含糊的“操作失败”;二是长度和换行,在小屏设备上不能截断成半句话。清理入口要测跳转是否正确、清理后名额是否立刻释放、清理过程中如果用户返回会不会状态错乱。
我遇到过一个很典型的问题:用户点“一键清理失效商品”,清理成功了,但页面上的角标数量没有刷新,用户看到还是 200,再点加购还是提示已满,直接放弃。这就是典型的“数据对了但视图没同步”,属于前端状态管理的疏漏,测试时如果只验接口返回码,完全发现不了。
5.3 交叉影响:上限会牵连哪些功能
上限不是孤立的,它会跟一堆功能产生交叉。
| 交叉功能 | 可能的问题 |
|---|---|
| 收藏夹 | 达上限后“移入收藏”按钮是否可用,收藏夹自身是否有上限 |
| 分享购物车 | 分享出去的清单是全部还是仅勾选项,超限时是否截断 |
| 批量加购 | 一次加 50 件但只剩 10 个名额,是全部失败还是部分成功 |
| 凑单推荐 | 达上限后推荐位是否还引导加购,点了会失败 |
| 降价提醒 | 失效区商品的降价提醒是否还推送 |
| 搜索加购 | 搜索结果页快速加购,超限提示是否明确 |
其中“批量加购部分成功”这个问题,特别值得单独设计一条用例。因为部分成功的语义很麻烦:是加进去 10 件然后提示“10 件成功,40 件因购物车已满未加入”,还是整体回滚一件都不加?前者对用户友好但有状态不一致风险,后者逻辑简单但体验差。无论选哪种,测试都要验证“提示的成功数量与实际加入的数量完全一致”,这个数字对不上就是 bug。
6. 常见问题与排查实录
6.1 问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 加购成功但购物车看不到 | 读写走了不同数据源,或缓存未失效 | 比对缓存与数据库记录,检查写入后的失效逻辑 |
| 购物车数量与角标不一致 | 角标统计口径含失效商品 | 统一统计口径,角标只算有效商品 |
| 全选后合计金额对不上 | 失效商品被计入或优惠未重算 | 打印勾选集合与计算入参 |
| 手动输入数量后页面卡死 | 输入未做类型校验,NaN 参与运算 | 检查输入过滤与回填逻辑 |
| 减到 1 再减没有反应 | 前端未处理边界,或后端返回被吞 | 抓包看请求是否发出 |
| 达上限后仍能加购 | 校验在写库之后,或并发未加锁 | 检查校验位置与锁粒度 |
| 删除商品后仍提示已满 | 缓存中的计数未更新 | 检查计数是实时算还是缓存 |
| 两端数量不一致 | 同步延迟或缺少版本号 | 检查同步机制与冲突解决策略 |
| 结算金额与购物车不一致 | 优惠在结算页重算,规则有差异 | 对齐两处的计算逻辑与规则版本 |
| 登录合并后商品丢失 | 合并策略覆盖而非累加 | 检查合并逻辑与去重键 |
6.2 几个我踩过的坑
坑一:用接口返回码判断成功,忽略了数据一致性。早期我写自动化用例,加购接口返回 200 就算通过,结果上线后用户反馈“加了三次只进去一件”。后来才知道是前端做了防抖,连点三次只发了一次请求,但用户预期是三次都生效。这类问题的本质是“防抖策略与用户心智不一致”,测试时必须模拟真实的连续点击行为,而不是串行调用三次接口。
坑二:忽略缓存与数据库的时序。购物车用了缓存,写操作是先写库再删缓存。如果删除缓存失败,用户会看到旧数据。这种问题在测试环境几乎无法复现,因为并发量小。我的做法是在测试环境人为注入缓存删除失败的模拟开关,专门验证降级逻辑是否合理。
坑三:把上限测试当成纯边界测试。我一开始只测了 199、200、201 三个点,全部通过。上线后出问题的是“已达上限时加购已有商品”,因为实现时先判断种类数是否超限,再判断商品是否已存在,顺序反了,导致老用户重复加购老商品也被拦。这个 bug 的教训是:边界值要跟业务状态的组合一起测,单点通过不代表逻辑正确。
坑四:数量输入的国际化问题。阿拉伯数字在某些输入法下会打出全角字符,用户复制粘贴时也可能带上零宽字符。前端如果直接用隐式类型转换,得到 NaN,页面上就会出现一串乱码或者直接白屏。这个坑我在两个项目里都遇到过,后来在所有数量输入框的用例里都加了一条“粘贴带空格和全角字符的文本”。
7. 自动化怎么落地:接口层为主,UI 层只留关键路径
7.1 分层策略
购物车的用例数量会随着功能迭代不断膨胀,全量 UI 自动化不现实,维护成本会把人拖垮。我的分层比例大概是:接口层占七成,服务层占两成,UI 层占一成。
接口层负责所有业务规则的验证,包括数量边界、库存联动、上限校验、价格计算、并发幂等。这层的用例执行快、稳定、定位问题准。服务层负责跨接口的流程编排,比如“加购 → 改数量 → 结算 → 取消订单”,验证数据在流程中的流转。UI 层只保留最核心的三五条主路径,用来做冒烟,确认页面能正常渲染和交互。
7.2 一个上限用例的接口层示例
下面这段是边界用例的写法示例,展示怎么把“已有 199 件再加速”这个场景表达清楚。重点是前置数据要显式构造,断言要看最终状态而不是只看返回码。
import pytest MAX_CART_KINDS = 200 @pytest.fixture def cart_with_n_kinds(client, user_token): def _build(n): # 先清空,避免历史数据干扰 client.post("/api/cart/clear", headers={"token": user_token}) resp = client.post( "/api/cart/batch_add", json={"item_ids": [f"item_{i}" for i in range(n)]}, headers={"token": user_token}, ) assert resp.json()["success_count"] == n return user_token return _build @pytest.mark.parametrize("exist, expect_ok", [ (MAX_CART_KINDS - 1, True), # 199,还能加 (MAX_CART_KINDS, False), # 200,已满 ]) def test_add_new_item_at_limit(client, cart_with_n_kinds, exist, expect_ok): token = cart_with_n_kinds(exist) resp = client.post( "/api/cart/add", json={"item_id": "brand_new_item", "qty": 1}, headers={"token": token}, ) body = resp.json() assert body["success"] is expect_ok # 关键:断言最终种类数,而不是只信返回码 kinds = client.get("/api/cart/count", headers={"token": token}).json()["kinds"] assert kinds == min(exist + 1, MAX_CART_KINDS) def test_add_existing_item_when_full(client, cart_with_n_kinds): token = cart_with_n_kinds(MAX_CART_KINDS) resp = client.post( "/api/cart/add", json={"item_id": "item_0", "qty": 1}, headers={"token": token}, ) assert resp.json()["success"] is True detail = client.get("/api/cart/list", headers={"token": token}).json() target = next(i for i in detail["items"] if i["item_id"] == "item_0") assert target["qty"] == 2这段代码里有两个细节值得注意。第一,test_add_existing_item_when_full这条用例专门验证“满了也能加已有商品”,这是前面说的那个真实 bug 的回归用例。第二,所有断言都落在最终状态上,而不是只看接口返回值,因为返回值和实际状态不一致正是最隐蔽的一类缺陷。
7.3 数据构造是自动化的生命线
购物车用例有一个天然难点:前置状态很难构造。要测 200 件满仓,难道手工点 200 次?所以必须有一个批量构造接口或者数据工厂。上面代码里的batch_add就是这个思路。
更彻底的做法是直接操作测试数据库,因为批量接口本身也可能有上限限制,导致无法一次造到 200。我的经验是:批量构造走数据库直插,业务验证走接口,两者分开,避免用被测逻辑去准备测试数据带来的循环依赖。
8. 最后聊几句个人体会
做测试这几年,我越来越觉得购物车这类“人人都懂”的功能,反而是最难测透的。因为它的复杂度不在单点,而在组合,在时间维度上的状态变化,在多端、多角色、多规则之间的相互作用。你写一百条用例能覆盖八成场景,剩下的两成才是真正决定线上质量的。
关于宝贝上限,我最大的体会是:任何一个看起来“不讲道理”的产品限制,背后通常都有一本算得很细的账。作为测试,如果你能把这本账算明白,你设计的用例就不会停留在“199 能加、200 不能加”这种表面,而是会去追问“失效商品算不算”“配置改了存量怎么办”“并发时锁够不够”。这三种追问,才是把用例从合格做到优秀的分水岭。
还有一个实操建议:把每次线上问题都反哺成用例。我有个习惯,每处理一个线上缺陷,就在用例库里加一条对应的场景,标注来源和日期。两年下来这个库从三百多条涨到八百多条,但它从来没让我在同一个坑里掉两次。这比任何方法论都管用。