前阵子有个刚转做测试的朋友来问我,说面试被问到一个题:给电商购物车设计测试用例,还要回答"为什么购物车要设宝贝上限"。他准备了一堆"能加购、能删除、能改数量"的常规答案,结果被追问"上限为什么是100而不是1000"时直接卡住了。这个问题其实很有意思,它表面上是一道用例设计题,实际上考的是两件事:一是你能不能把一个看似简单的功能拆成状态机去测,二是你能不能用产品、前端、后端、风控四个视角去解释一个业务规则的存在理由。购物车是电商App里日活渗透率最高的页面之一,几乎所有交易链路的异常都会在这里先冒头,所以它既是测试新人的入门题,也是老手的照妖镜。下面我把自己实际做购物车测试时的一套思路完整拆开,包括用例怎么分层、上限背后的成本账怎么算、以及那些文档里不会写但线上一定会遇到的坑。
1. 先搞清楚购物车在测什么:一张状态机比一百条用例更重要
很多人拿到"购物车测试用例"这个题目,第一反应是打开Excel开始列:加入购物车、删除、修改数量、全选、反选、去结算。列了五十条,看起来很勤奋,但真跑到线上,漏掉的问题往往出在状态迁移上,而不是功能点上。我的习惯是先不写用例,先把购物车画成一张状态机。
1.1 从加购到下单,一辆购物车至少经历六个状态
一个商品条目进入购物车之后,它的状态远比"存在/不存在"复杂。我通常按这几层去切:未加购 → 已加购(有效)→ 已勾选/未勾选 → 库存不足/价格变动 → 失效(下架、售罄、活动结束)→ 已结算移除。这六个状态之间不是单向流动的,失效商品可以被重新上架拉回有效态,勾选状态会在商品失效时被强制取消,结算失败又要回滚到未结算。
状态机画出来的价值在于,你会立刻发现一批"迁移路径"用例,而这些恰恰是最容易漏的。比如:一个商品在购物车里放了三小时,期间价格降了又涨回去,界面上的"降价提醒"标签该不该消失?比如用户勾选了一件商品,然后这件商品被下架,勾选态要不要自动清掉、总价要不要重算、结算按钮要不要置灰?再比如批量勾选十件商品去结算,其中三件库存不足,剩下七件是继续下单还是整单失败?
这些问题的答案在不同平台不完全一样,但测试的职责不是猜答案,而是把问题抛给产品确认,然后把这个确认结果变成一条可回归的用例。我见过太多团队,规则只存在于某个人的脑子里,人一走,用例就失效了。
我一般会把状态迁移画成一张表,行是"当前状态",列是"触发动作",格子里填"动作后状态 + 用户可见反馈"。这张表一旦定稿,用例的骨架就出来了,剩下的是往里填边界值。
1.2 把业务规则盘点成一张可执行的测试点清单
状态机解决的是"流程对不对",业务规则解决的是"算得对不对"。购物车的规则密度高得离谱,我通常按四个维度盘点:商品维度、价格维度、优惠维度、账号维度。
商品维度包括:是否下架、是否售罄、是否限购(每人限N件)、是否预售、是否仅剩最后几件、是否属于不同店铺、是否需要单独配送。价格维度包括:原价、活动价、会员价、阶梯价、价格保护期内的降价。优惠维度包括:店铺满减、平台跨店满减、优惠券、包邮门槛、赠品、积分抵扣。账号维度包括:登录态与未登录态、新老用户差异、不同会员等级、不同收货地址(影响运费和可配送范围)。
这四类规则两两组合,理论上会爆炸,实际测试里不可能全测。我的取舍原则是:规则本身的单点必测,规则之间的交互只测"同一条链路上会同时生效"的组合。比如"限购"和"数量修改"必须在同一条链路上,因为用户改数量时就会撞上限购;但"限购"和"运费计算"就没有强关联,不必强行组合。
2. 单商品维度:那些看起来最无聊、最容易漏的用例
单商品场景是购物车用例的地基。新手往往觉得这部分"太简单没什么好测",但实际上线后用户投诉最多的就是这一类:加不进去、加进去了价格不对、勾选了却没算钱。
2.1 商品状态的四个命门:下架、售罄、失效、限购
下架的测试点不只是"下架后购物车里显示失效",还要测时序:用户停留在购物车页面时商品被下架,页面会不会实时刷新?用户点了结算但还没支付时商品下架,订单能不能创建成功?前者考的是轮询或长连接的时效,后者考的是下单接口的服务端校验是否兜底。
售罄比下架更微妙,因为它是动态的。我测过一个经典场景:购物车里剩最后2件,A用户和B用户同时勾选结算,谁先提交谁成功,后提交的应该收到"库存不足"的明确提示,而不是一个500错误或者一个静默失败。这里的判定标准是:失败必须有可读的原因,且购物车状态不能脏。
失效是一个聚合概念,包含下架、售罄、活动结束、店铺关闭、商品被删除。失效商品在购物车里通常有两种处理:置底折叠展示,或者直接隐藏但保留在"失效宝贝"入口。两种都要测,而且都要测失效商品是否参与总价计算、是否可以被勾选、是否占用宝贝上限名额。最后这一点非常关键,后面讲上限时会展开。
限购是最容易出bug的一个。限购规则通常有"每单限购"和"每人限购"两种口径,用户在购物车里改数量时,输入框的最大值应该被限购数约束,但很多实现只做了前端限制,用户手动输入或者调接口就能绕过。所以限购的用例一定要包含绕过前端直接调接口这一条,验证服务端的最终校验。
2.2 价格与优惠的三个时刻
购物车里的价格从来不是"一个数",而是三个时刻的三个数:加购时的价格、进入购物车页面时的价格、点击结算时的价格。理论上应该以结算页为准,但用户的心理预期是以加购价为准,这个落差就是投诉的来源。
我测价格时会重点验证三件事。第一,价格变动后购物车是否有提示(通常是"较加入时降价X元"或"涨价了"的标签),且这个提示的计算基准是加购时快照价,不是上一次刷新的价格。第二,价格变动是否影响已勾选商品的总价实时重算,且重算逻辑与结算页一致。第三,价格在活动切换的整点(比如零点)前后是否会出现短暂的错乱,这是最容易出问题的时间窗口。
优惠的测试重点在分摊。多商品参与同一个满减活动时,优惠金额要按各商品的金额比例分摊到每一行,退款时按分摊后的金额退。这里有个高频bug:分摊算法用了浮点数直接四舍五入,导致各行分摊额加总后与总优惠差了一分钱,结算页显示"优惠100元",各行加起来是99.99元。这种一分钱的差异在财务对账时会被放大成事故。
2.3 单维度用例的落地表格
我把单商品场景整理成下面这张表,实际项目里可以直接改成用例集:
| 场景 | 操作 | 预期结果 | 易漏点 |
|---|---|---|---|
| 正常加购 | 点击加购 | 条目出现,数量+1,角标+1 | 重复点击是否重复加购 |
| 已达单SKU数量上限 | 继续加购 | 提示已达上限,数量不变 | 上限值是否与限购数冲突 |
| 商品下架后加购 | 加购已下架商品 | 提示商品已失效,不入车 | 直接调接口是否被拦截 |
| 购物车停留期间商品失效 | 停留30秒后刷新 | 条目置灰/折叠,勾选态清除 | 总价是否同步重算 |
| 修改数量为0 | 输入0或点减号到0 | 提示是否删除该商品 | 不同平台的交互不一致 |
| 修改数量超限购 | 输入999 | 前端截断 + 服务端拒绝 | 只做前端限制是典型漏洞 |
| 价格变动 | 活动价切换 | 展示降价/涨价标签 | 基准价取的是快照价 |
| 多件同SKU | 同一商品加购3次 | 合并为一行,数量=3 | 不合并会撑爆上限名额 |
| 未登录加购 | 游客态加购 | 存本地,登录后合并 | 合并冲突与超限处理 |
| 删除后重新加购 | 删除再添加 | 回到队尾还是原位置 | 排序规则影响体验 |
3. 多商品叠加:购物车真正的复杂度都在"组合"里
单商品测试是体力活,多商品组合才是脑力活。购物车的很多规则在设计上是"可叠加"的,叠加之后就会出现优先级、互斥、门槛临界值这些麻烦事。
3.1 凑单、满减、包邮的门槛计算
满减和包邮的门槛是典型的"边界敏感"逻辑。用户购物车里总价99元,满100减20,页面上会提示"再买1元可减20元"。这个提示本身要测:差额计算是否只统计参与活动的商品?运费是否计入门槛?优惠券抵扣后的金额是否计入门槛?
更麻烦的是多档满减的择优。满100减20、满200减50、满300减80,用户购物车总价250元,系统该选哪一档?一般是选优惠力度最大的,但有些实现是"够哪档用哪档"从上往下匹配,结果250元用了满100减20,用户亏了。这类逻辑测试必须构造临界值:99/100/101、199/200/201、299/300/301,每一组都要验证选中的档位和最终实付。
包邮门槛还有个隐藏坑:部分商品不参与包邮。购物车里A商品参与包邮活动、B商品不参与,总价过了门槛,运费该怎么算?是按参与商品的金额判断,还是按全车金额判断?这个规则必须跟产品确认清楚,因为不同平台的策略不一样,而且经常会在版本迭代中悄悄改掉。
3.2 混合结算:部分勾选与拆单
购物车支持部分勾选结算,这带来一个衍生问题:优惠是按全车算还是按勾选商品算。用户购物车里有5件商品,只勾选其中2件去结算,满减门槛是按5件算还是按2件算?正确答案通常是按勾选商品算,但很多实现在购物车页面展示的"已优惠X元"是按全车算的,导致用户看到的价格和结算页对不上。
拆单是另一个高发区。不同店铺的商品必须分开下单,不同配送方式(次日达/普通快递)可能分开,预售和现货必须分开,跨境和国内必须分开。测试时要验证:拆单后每单的运费是否独立计算、每单的优惠是否独立分摊、拆单后的订单总数与购物车勾选数是否一致。我遇到过拆单后优惠券被重复使用的bug,一张券在三个子订单里都抵扣了一次,这种问题在小流量下很难发现,一到大促就是资损。
3.3 库存争抢下的"半成功"
批量结算最容易出现"半成功":勾选10件,8件下单成功,2件库存不足。这时候产品要给出明确策略,是整单失败还是部分成功。大部分平台选择部分成功,但那2件的购物车状态要怎么处理?是保留在购物车并标红提示,还是直接移除?保留的话,用户再次结算会不会重复下单?
我的验证方法是构造一个可控的库存环境:把某个SKU的库存手动改成1,然后用两个账号同时结算同一商品,观察后提交方的表现。判定标准有三条:失败提示必须明确到具体商品、购物车状态必须可恢复、不能产生重复订单。这三条只要有一条不满足,就是P1以上的问题。
4. 宝贝上限的由来:不是一个拍脑袋的数字
现在进入正题的另一半:为什么购物车要设上限。我面试时问过不少人,大部分回答是"防止用户加太多",这个答案等于没说。真实的原因是多方面成本核算的结果,我从下往上拆。
4.1 客户端:一个撑不住的列表要付多少代价
购物车页面看起来只是一个列表,但每一行的渲染成本远高于普通列表项。一行购物车要展示商品图、标题、规格、价格、原价、优惠标签、库存提示、数量控件、勾选框,还要参与总价实时计算。这意味着每加一件商品,客户端不只是多渲染一个View,还要多算一次价格、多挂一组事件监听。
假设一个中等复杂度的购物车行占用约80KB内存(图片解码后的大头),100行就是8MB,加上列表复用池、图片缓存、优惠试算的中间对象,整页内存峰值很容易冲到50MB以上。中低端机型的单应用内存红线通常在200MB到300MB之间,购物车占掉四分之一,后面的结算页、订单页就没法活了。
渲染耗时也是硬约束。列表首屏渲染要控制在16ms一帧的预算内,100行的滚动流畅度依赖复用机制,但如果每行都带独立的倒计时、库存轮询、优惠试算,复用就会被打断。所以上限首先是一个性能保护阈值。
4.2 服务端:一次全量查询的成本账
用户打开购物车时,服务端要做的事包括:查购物车条目、查每个SKU的最新价格、查库存、查可用优惠、试算最优优惠组合、查商品状态(是否下架)、查店铺活动。条目数从10涨到100,这些查询的复杂度不是线性增长的。
价格和库存可以批量查,这部分是线性的。但优惠试算是指数级复杂度的:跨店满减要和店铺满减组合,优惠券要判断门槛和互斥,还要考虑分摊。10件商品的可能组合数已经不小,100件商品如果做全量穷举,单次请求的CPU时间会直接顶到超时。实际工程里当然会用贪心或者剪枝来近似,但条目越多,近似结果偏离最优越远,用户就越容易觉得"系统没给我算最便宜的"。
再看存储和带宽。一个有1亿日活的平台,如果人均购物车条目从20涨到200,购物车表的行数直接翻十倍,缓存命中率下降,DB的读写压力线性上涨。带宽也是同理,购物车接口的响应体从几十KB涨到几百KB,弱网环境下的加载时间会被用户直接感知到。
4.3 营销与风控:上限是一道天然的闸门
还有一层不太被提及但很实际的原因:上限本身就是风控手段。购物车在部分场景下会占用资源,比如秒杀商品加购会短暂预占库存、限购商品加购会被计入风控计数。如果允许无限加购,脚本就可以批量囤货、批量占库存,把正常用户挤出去。
上限还能防住一类灰色玩法:把购物车当收藏夹用。有些用户习惯先把几百件商品塞进购物车"慢慢挑",这会让购物车的数据失去推荐价值,也会让"加购转化率"这个核心指标失真。设一个上限,相当于强制用户做取舍,加购行为才具备真实的购买意图信号。
4.4 体验层:100件之后,购物车已经不是购物车了
最后一个原因是产品层面的。购物车的核心价值是"缩短决策到支付的路径",条目越多,用户在里面翻找、比价、删除的时间就越长,决策成本反而上升。实际数据里,购物车条目数和转化率并不是正相关,超过一定数量后转化率会明显下滑。所以上限同时也是在替用户做减法。
常见平台的数值大概在100到200之间,单SKU数量上限通常在99。为什么单SKU是99而不是100?一部分原因是UI上数量控件的宽度只留了两位数的空间,三位数会破版;另一部分原因是行业惯例,用户在心理上对"99"的接受度更高,超过99会有"买这么多是不是有问题"的疑虑。这个数字没有绝对的对错,但一旦定下来,就不能随便改,因为它是被写进无数条用例和用户心智里的。
5. 上限相关的测试:边界值不是只测99、100、101
理解了上限的来源,用例才写得准。上限测试的难点不在于取边界值,而在于上限会和其他规则交叉,交叉点才是bug的聚集地。
5.1 上限规则的三种表现形式
同样是"上限100件",实现方式可能有三种,测试策略完全不同。
第一种是硬上限:加第101件时直接拦截,提示"购物车最多放100种宝贝"。这种最好测,边界就是99、100、101。
第二种是软上限:允许超过,但超过后部分商品被折叠、不参与优惠、或者需要手动"清理"。这种要测折叠逻辑、折叠后总价计算、以及折叠商品能否被勾选。
第三种是分状态上限:有效商品和失效商品分开计数。比如有效商品最多100件,失效商品另算。这种最容易出bug,因为失效商品转有效时可能瞬间突破上限。用例要覆盖:99件有效 + 5件失效,此时把5件失效商品重新上架,系统该怎么处理?
5.2 触顶后的状态一致性
触顶之后的一致性问题是重灾区,我列几个必测的:
- 满额后加购已有商品:购物车已有100种商品,其中包含商品A,此时再对A加购一件。这不增加种类数,应该成功,数量+1。很多实现会错误地走到"种类上限"分支直接拦截。
- 满额后删除再加购:删掉1件变成99种,再加剧1件应该成功。要测删除后计数是否实时更新,而不是等刷新。
- 满额后商品失效:100种商品里有1件失效,失效商品如果占用名额,用户就永远加不进新商品,体验极差。这种情况要么给失效商品单独的存储区,要么在失效时释放名额。
- 未登录转登录后的合并超限:游客态本地存了50件,登录后服务端已有80件,合并后130件超限。合并策略是按时间取最近的100件,还是提示用户手动选择,还是直接拒绝合并?这个场景在换手机登录时非常常见。
- 多端同时加购到临界值:手机端加到99,平板端同时加2件,最终结果是99还是101?这考验服务端的原子性,通常要靠行锁或者版本号,测试时要并发触发。
5.3 上限与其他规则的交叉
把上限和前面讲的规则叠起来,问题会成倍增加。比如:上限和限购交叉——100件商品里有一件限购3件,用户在最后一件商品上超了限购,提示语该说"超限购"还是"购物车已满"?再比如上限和优惠交叉——满100件商品恰好能触发一个"满100件打9折"的活动,这个活动是真实存在还是纯粹的测试干扰项?如果真实存在,上限设计时就要避开这个数。
还有一类很隐蔽的问题:上限值本身是配置化的,运营能在后台调。那么调小之后,已有用户的购物车条目超过新上限时,是提示用户清理,还是只拦截新增?这个降级策略必须在需求评审时就问清楚,否则上线后流量一压,用户会发现自己的购物车"只出不进"。
6. 我实际跑这套用例的顺序,以及几个绕不过去的坑
讲完设计思路,说一下实际执行。购物车用例的回归频率很高,几乎每个版本都要跑一遍,所以数据构造的效率直接决定了你加班到几点。
6.1 造数据:怎么快速堆出99种商品
手动点99次加购是最蠢的办法,十分钟就过去了还容易点错。我的常规做法是准备一个测试商品的批量脚本,用接口一次把100个不同SKU加进购物车,然后针对临界的那几个SKU做手工微调。脚本里要注意两件事:一是每个SKU必须是不同商品,同SKU会合并;二是加购时要带上正确的用户标识和渠道参数,否则可能落到不同的购物车分片里,导致实际数量和预期对不上。
对于"满额后加购"这类场景,我会准备两个账号,一个账号的购物车固定保持在99件,另一个保持在100件,每次回归直接登录对应账号开始测,省掉重复构造的时间。这套"状态账号"在长期项目里非常值钱。
6.2 多端同步与并发加购
多端同步是我踩过最多坑的地方。手机端删了一件商品,网页端刷新后还在;网页端改了数量,App端角标没更新。这类问题的根因通常是缓存策略不一致,或者同步消息的推送有延迟。测试时要在两端同时操作,观察最终一致需要多久,超过2秒基本就会被用户感知到。
并发加购的测试要点是临界值附近的竞态。我会用压测工具同时发10个加购请求到一件商品,验证最终数量是正确的10还是出现了少数丢失。如果实现是先查再写,很容易出现丢更新;正确做法是原子自增或者乐观锁重试。这个点在小流量下测不出来,必须主动构造并发。
6.3 一份可以带走的漏测自查清单
最后把我每次发版前都要过一遍的清单放出来,基本都是血泪换来的:
- 无效商品是否占用了宝贝上限名额?
- 单SKU数量上限和种类上限冲突时,提示语是否正确?
- 登录合并购物车超限时,是否有明确的用户提示?
- 价格变动标签的基准价,是加购快照价还是上次刷新价?
- 多商品满减分摊后,各行金额加总是否等于总优惠?
- 部分勾选结算时,优惠门槛按勾选商品算还是全车算?
- 拆单后,优惠券是否被子订单重复使用?
- 批量结算部分失败时,成功的订单是否去重、失败的商品是否保留?
- 上限配置调小后,存量超限用户能否正常删除、结算?
- 购物车角标数量与实际条目数是否一致(尤其在有失效商品时)?
这张单子我用了两年多,几乎每次发版都能从里面捞出至少一条问题。购物车这个功能的可怕之处在于它太简单了,简单到所有人都觉得不用测,但它又是交易链路的第一站,任何一个数据不一致都会被放大到订单和售后。我现在带新人,第一课就是让他们把这张状态迁移表和这份清单背下来,剩下的用例设计能力,靠的是对业务规则一遍一遍的追问。