上一篇把无穷多的取值压成了七条用例。那张表已经能交给别人执行了,但真拿到手上,执行的人马上会追问:账号是什么状态?购物车里有没有东西?输完之后,屏幕上出现什么才算这条过了?
这篇回答的就是这些追问。一条用例光有“取什么值”不够,它还得写清起点、动作和判定标准,才能真正被别人跑一遍并且得出同一个结论。
用例是三块拼图:起点、动作、判定标准
ISTQB 给测试用例(test case)的定义是:为某个特定目标或测试条件而设计的一组输入值、执行前置条件、预期结果和执行后置条件 1。换句话说,它要回答三个问题——
- 开始跑之前,系统处在什么状态?(前置条件)
- 我具体做什么?(操作步骤)
- 做完之后看见什么,才算这条过了?(预期结果)
这三个问题对应测试这件事的底层结构:测试不能只“操作一下看看”,它必须在操作之前就知道“什么事情算对”。少了第三块,你手里有的只是一份操作说明,不是一条用例 2。
前置条件:把起点钉死,别让别人来猜
前置条件(precondition)是执行这条用例之前系统必须已经满足的状态。它的作用很具体:把起点固定住,让不同的人跑出来是同一个结果。
为什么它经常被忽略又经常出事?因为同一串操作,起点不同结果就不同。用“管理员把用户从 Agent 改成 Manager”这条用例举例:如果账号权限、被改的那个用户当前的角色、是否已经登录过都没写清,另一个人按步骤点一遍,可能改的是一个权限不同的用户,也可能因为权限不足根本进不去后台。这时候他既不能判定通过,也不能判定失败——他卡在了起点上。这类“无法复现”的麻烦,常见源头就是前置条件缺失或含糊 5。
前置条件该写哪些东西?可以按下面几类过一遍:
- 账号与权限:用哪个角色的账号登录,这个账号有没有权限进这个页面;
- 已有数据:订单里有没有商品、金额多少,优惠码是什么状态,被操作的对象当前是什么值;
- 环境与开关:在哪个环境、哪个浏览器;有没有依赖某个功能开关、某条配置;
- 上一条用例留下的状态:如果这条用例依赖前一条的结果,必须写明。
最后一类在正式的用例规格模板里也有它的位置,IEEE 829 就专门要求记录这类用例之间的依赖关系 4。判断标准很简单:照着你写的前置条件,能不能一步步把系统摆到起点状态。 如果执行的人需要回头来问你,就说明还缺东西。
步骤:一步一个能看见的动作
步骤是执行的人照着做的指令。它有两条要求。
第一条,一步只放一个动作。“登录并进入设置页”是两个动作,写成一个步骤,出问题时你不知道是登录挂了还是跳转挂了。“导航到设置”这种写法更麻烦——它要求执行的人自己理解“设置”在哪儿,不同的人会点不同的路径 5。
第二条,用界面上的确切文字,别用自己的概括。 写“点击‘保存’”比写“提交表单”好,写“点击‘继续结算’按钮”比写“进入下一步”好。理由是界面上的文案就是一个可观察的事实,而“下一步”是你的理解。这一点在把用例交给别人、或者以后要改写成自动化脚本的时候尤其明显:自动化工具只会照字面执行,含糊的步骤让它无从下手。
还有一个容易漏的情况:当“不做什么”是关键时,也要写出来。 比如“在邮箱地址一栏留空,不输入任何内容”。留空本身是一个动作,不写清楚,执行的人很可能顺手填了。
如果一组用例都要先登录,把这段收进前置条件或公共步骤就行。要控制的是“可复现”这条底线,不是字数。
预期结果:判定权不能落在执行人手里
现在说这篇的重点。为什么“正常显示”“页面加载成功”“系统按预期工作”不算预期结果?
因为这三句话把判定权交还给了执行的人。 他看到页面之后,要自己决定“这算不算正常”。而不同的人对“正常”的标准不一样,于是同一条用例,在不同人手里、甚至同一个人不同时间跑,会得出不同的通过/失败结论。一份用例的结果一旦不稳定,它留下来的记录也就没了意义——你没法从“上次通过、这次失败”里读出任何信息,因为两次的判定尺子不一样 5。
更要紧的是,缺陷正好藏在“看上去正常”的缝隙里。 提示文字写错了一个字、金额少算了一分、页面弹出“保存成功”但数据其实没入库、跳转对了但列表里没出现新记录——这些画面在“正常显示”这四个字底下全都算通过。预期结果写得越粗,能判定出来的缺陷就越少。
还有一层原因,是测试预言的问题。 如果你不在执行前写下预期结果,而是先看系统实际做了什么、再回头把那个结果填写成“预期”,那你就等于宣布“凡是发生的就是应该发生的”。ISTQB 的说明很直接:预期结果应当在执行之前定义,否则一个貌似合理、其实是错误的结果,很可能被当成正确结果接受下来 3。所以预期结果不是执行完之后补的注释,它是用例的一部分,先写。
那一条好的预期结果长什么样?它要能被对照着看,具体到执行的人不需要做判断 5。可以从这几个角度写:
- 屏幕上出现什么:确切文案,比如红色提示“抵扣比例需为 1 到 100 的整数”,出现在哪个字段下方;
- 停在哪儿:页面是否跳转、跳转到哪个页面;
- 数据变成什么:这条记录的值、订单总额、列表里多出的一行。数据的变化往往比界面更能说明问题;
- 不该发生什么:表单没有提交、没有生成新订单、原数据没有被改动。
把“不该发生什么”写出来是很有价值的一步。很多缺陷的表现恰恰是“该做的没做”,而如果你只写了“应该出现什么”,执行的人看到提示出来了就判定通过,不会再去确认数据到底存没存。
一条用例该覆盖多少
回到上一篇那个抵扣比例字段(1 到 100 的整数)。等价类加边界值给出了七八个要测的值,那么“输入 0 被拒绝”“输入 101 被拒绝”“输入 abc 被拒绝”应该写成三条用例,还是一条?
默认答案是三条。 ISTQB 的定义里,一条用例是“为某个特定目标”设计的 1。如果一个用例里塞进三个彼此独立的结果,会有两个后果:一是它失败时,你只知道“这条坏了”,不知道是哪个输入坏在了哪一步;二是执行的人常常在第一个检查点不通过时就停下来报错,后面的检查根本没跑到,等于白丢了信息 6。
这里有个很好用的操作判据:把用例标题读出来。如果需要用“而且”来连接两件事,就该拆。“输入 0 被拒绝,而且输入 abc 被拒绝”听上去就是两条用例。反过来,“提示文字正确,而且输入框标红”是同一件事的两个侧面,可以留在一条里——它们描述的是同一个结论,不是一个通过、一个失败各自独立 6。
再补一句和上一篇的衔接:测试点和用例不是一对一。“抵扣比例越界应被拒绝”是一个测试点,它可以派生多条用例,每条用一个具体数据去踩它。测试点说的是要验什么,用例说的是用什么数据、怎么做、验到什么算过。所以用例条数变多,不代表需求变多了。
那什么时候可以少写几条?如果这些检查点是同一结论的不同侧面,或者成本很高(比如每次都涉及一笔真实支付),把它们合进一条用例是合理的取舍。但只要它们可能各自独立地坏掉,就分开。
走一遍:把那条用例写出来
还是用优惠码需求。假设后台可以配置某个优惠码的抵扣比例,规则是 1 到 100 的整数,越界拒绝保存。上一篇已经定下要取 0 这个下界外侧的值。写成一条用例就是这样:
| 项目 | 内容 |
|---|---|
| ID | TC-DISCOUNT-002 |
| 标题 | 抵扣比例输入 0 时保存被拒绝,原比例不变 |
| 前置条件 | 以管理员账号登录后台;已存在优惠码 SUMMER20,当前抵扣比例为 20;该账号有优惠码配置权限 |
| 步骤 | 1. 打开 SUMMER20 的编辑页;2. 将“抵扣比例”字段清空,输入 0;3. 点击“保存”按钮;4. 刷新当前页面 |
| 预期结果 | 第 3 步后页面停留在编辑页,不跳转;出现红色提示“抵扣比例需为 1 到 100 的整数”;第 4 步刷新后,SUMMER20 的抵扣比例仍为 20,未被改成 0 |
| 后置条件 | 无数据变更,无需清理 |
对照一下常见的坏版本:
标题:优惠码测试 前置条件:(空) 步骤:1. 试几个值看看有没有报错 预期结果:显示正常
两个版本的差别不在于篇幅,而在于 能不能被别人照着跑出同一个结论。坏版本没有起点,步骤里“试几个值”是执行人自己决定的,判定标准也落在他的判断里。这样的用例写在文档里,看起来覆盖率是满的,实际上什么也没锁住。
写多细,停在哪里
最后说清楚这套写法的代价,免得把它用成负担。
这套写法锁定的是 需要复现、需要交接 的场景:要交给别人的用例、进回归集的用例、以后可能被改写成自动化脚本的用例。它换来的是别人能独立执行、结果能比较、缺陷能被稳定复现。
代价是维护成本:界面文案一改,步骤和预期结果里的文字就得跟着改。所以不必所有用例都写到同一种颗粒度。高风险、要交接、要进回归集的用例写细;只给自己看一眼、跑完就不再用的检查,写要点就够。
至于用例执行之后怎么把失败写成一份别人看得懂的缺陷报告,那是下一篇的事。