简介:一份适合软件测试初学者与测试工程师参考的用例设计实例文档,围绕黑盒测试、等价类划分、边界值分析、错误推测、因果图与判定表驱动等经典方法,给出可直接套用的用例模板与设计思路。文档先梳理了优秀用例应包含的十二项基本信息,再以B/S结构登录功能为对象提供完整的黑盒测试用例表,覆盖用户名密码校验、输入容错、连续三次失败自动关闭等场景。三角形判断程序展示了等价类划分的完整分析过程;NextDate函数用例清晰演示边界值选取方法;错误推测法围绕排序程序给出空表、单元素、逆序、重复值等异常场景;因果图法以自动售货机找零为例梳理原因与结果关系。资源共一个doc文件,压缩包约329KB,排版紧凑、便于打印阅读。已有4775人学习下载这份文档,适合作为测试用例设计入门或编写测试文档的参考模板。
1. 软件测试用例的经典实例:为什么别人能直接套用,你写出来却总被评审打回
评审会上,A同学把登录用例铺在桌面上:“输入abc123,点击登录,断言成功。”会议室安静了三秒——这就是那条要被复用的经典实例吗?我当时心里想的是:这只能算操作记录,测不出任何边界问题。软件测试用例的经典实例,价值不在于“记录步骤”,而在于把一段需求翻译成一组可判定、可回归、可追溯的验证任务。这篇笔记按我平时写用例的顺序,把测试设计方法、登录/支付金额/搜索三个高频场景的完整用例、评审里踩过的坑和参数化改造一次讲清楚。适合刚转测试的开发、正在整理用例库的测试工程师,以及给团队定用例规范的负责人。
2. 用例设计方法怎么选:等价类、边界值、判定表与场景法的适用边界
很多人拿到需求就开写,结果用例要么多到跑不完,要么漏掉最关键的边界。我一般先问自己一个问题:被测对象最核心的逻辑形态是什么?是单个输入值的合法性校验,还是多个条件组合触发的分支,还是对象状态随操作流转?这个问题的答案决定了用什么设计方法。
2.1 等价类划分:先确定“合法域”再谈“有效无效”
等价类划分的核心是把输入域按处理逻辑分成若干类,每一类里取一个代表值。拿一个常见的年龄校验逻辑举例,假设模拟项目X的报名系统要求年龄必须是18到60之间的整数,被测函数长这样:
def validate_age(age: int) -> tuple[bool, str]: # 先排除布尔值:Python 里 bool 是 int 的子类,True/False 会被当成 1/0 if not isinstance(age, int) or isinstance(age, bool): return False, "年龄必须是整数" if age < 18 or age > 60: return False, "年龄不在可报名范围" return True, "ok"这里的第一步不是把数据分成有效/无效,而是先划出“合法域”:年龄的合法域是 [18, 60] 的整数。有效等价类就是18-60之间的任意整数,无效等价类有三类:小于18的整数、大于60的整数、非整数。大多数人在这一步犯的错是只凭直觉分,而不是先看需求原文。需求里写“18到60”,那“60”到底算不算有效取决于是否写明“含”,没写就得找产品确认,不要自己猜。
等价类设计的产出是一组代表值,每个值对应一类。等会儿写用例表时,我会在每条用例后面标注它覆盖的等价类,方便做需求追溯。这一步的常见误用是把“看起来合法的输入”当成“有效等价类”,比如看到60岁觉得“接近边界了可能有问题”就放进有效类,然后边界值设计时又把它当边界用,等于同一场景测了两遍。
2.2 边界值分析法:边界不是单点,而是“界内界外各取一组”
边界值分析最容易被简化成“取最小值和最大值”,这不对。我习惯在每一个边界上取两个点:一个恰好落在合法域内,一个恰好落在合法域外。还是年龄的例子:
| 用例编号 | 输入年龄 | 预期结果 | 覆盖点 |
|---|---|---|---|
| TC-AGE-001 | 17 | 拒绝 | 下边界外 |
| TC-AGE-002 | 18 | 通过 | 下边界内 |
| TC-AGE-003 | 60 | 通过 | 上边界内 |
| TC-AGE-004 | 61 | 拒绝 | 上边界外 |
| TC-AGE-005 | 30 | 通过 | 等价类代表 |
这里有两个细节经常翻车。第一,如果需求写“大于18且小于60”,那么有效边界变成了19和59,18和60反而成了出界点,和“18(含)到60(含)”的用例完全不同。第二,对于浮点数类型,边界值要考虑到精度,比如金额边界是0.01,那0.00和0.011这种带更多小数位的输入都要纳入。我一般会在设计前和开发确认字段类型和是否允许精度误差,否则边界值用例会被decimal计算方式干扰。
2.3 判定表与场景法:多条件组合和状态流转怎么落到用例
当一个功能有多个条件、且每个条件组合对应不同结果时,等价类就不够用了。拿运费规则举例:会员(是/否)、订单金额满100元(是/否)、是否加急(是/否),三个条件两两组合后产生不同的运费策略。判定表就是先把所有条件组合列全,再填动作。条件2×2×2=8条,穷举并不可怕,可怕的是漏掉其中一条。判定表做完之后还要检查有没有可以合并的项,比如“非会员且金额未满100,无论是否加急都收20元运费”,这两行动作一样,可以合并成一条规则。
场景法则是应对“对象状态随操作流转”的情况。最典型的实例是订单状态:待支付、已支付、已发货、已完成、已取消。场景法要求用例覆盖正常流转路径和非法跳转路径,比如待支付状态直接到已完成应该被拒绝。这里的关键是识别状态之间的“合法迁移边”,每一条边就是一条用例,再补上“同一状态下重复操作”的幂等用例。选型结论也很直接:单个输入值校验用等价类加边界值,多条件组合用判定表,状态流转用场景法。经典实例里前三种方法占了八成以上的场景。
3. 三个高频场景的用例实例:登录、支付金额、搜索查询从需求到用例表
场景选的是三类几乎每个系统都有、且最能体现测试设计差异的功能:登录是典型的输入校验加上状态锁定,支付金额是精度与边界问题重灾区,搜索查询则在字符处理和分页上暗藏玄机。每个场景我都会按“需求→被测接口→用例表→参数说明”的顺序展开。
3.1 登录功能实例:格式校验、口令强度和锁定策略
模拟项目X的登录需求是这样写的:用户名6-20位,只能包含字母、数字和下划线;密码8-30位,必须同时包含大小写字母和数字;验证码是4位数字,5分钟内有效;连续输错密码5次,账号锁定30分钟。对应被测函数的关键逻辑:
import re def login_check(username: str, password: str, captcha: str, user_store: dict, captcha_store: dict) -> dict: # 注意:不能用 \w,它在 Python 中会匹配到中文字符 if not re.fullmatch(r'[A-Za-z0-9_]{6,20}', username): return {"code": 40001, "msg": "用户名格式错误"} if not re.fullmatch(r'(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,30}', password): return {"code": 40002, "msg": "密码复杂度不足"} if captcha_store.get(username) != captcha: return {"code": 40003, "msg": "验证码错误"} if user_store.get(username, {}).get("failed_count", 0) >= 5: return {"code": 40004, "msg": "账号已锁定"} return {"code": 0, "msg": "ok"}这里第一个坑就是正则里的 \w,很多人习惯用它匹配字母数字下划线,但它会连带匹配中文字符,导致中文用户名被放行。所以我换成字符组。接着看用例表:
| 用例编号 | 用户名 | 密码 | 验证码 | 预期code | 设计点 |
|---|---|---|---|---|---|
| TC-LOGIN-001 | abcd(5位) | Abcd1234 | 1234 | 40001 | 用户名下边界外 |
| TC-LOGIN-002 | abcdef(6位) | Abcd1234 | 1234 | 0 | 用户名下边界内 |
| TC-LOGIN-003 | abcdefghijklmnopqrst(20位) | Abcd1234 | 1234 | 0 | 用户名上边界内 |
| TC-LOGIN-004 | 长度21位的用户名 | Abcd1234 | 1234 | 40001 | 用户名上边界外 |
| TC-LOGIN-005 | 含中文的用户名 | Abcd1234 | 1234 | 40001 | 非法字符 |
| TC-LOGIN-006 | abcdef | Abcd123(7位) | 1234 | 40002 | 密码下边界外 |
| TC-LOGIN-007 | abcdef | abcd1234(纯小写数字) | 1234 | 40002 | 缺大写字母 |
| TC-LOGIN-008 | abcdef | Abcd12345678 | 1234 | 0 | 密码合法 |
| TC-LOGIN-009 | abcdef | Abcd1234 | 123(3位) | 40003 | 验证码位数不够 |
| TC-LOGIN-010 | abcdef | Abcd1234 | 错误5次后的新码 | 40003 | 验证码失效 |
设计登录用例时要特别注意前置状态:账号锁定用例、验证码失效用例都会改变服务端状态,跑完必须清理数据,否则后续用例被污染。第四列“预期code”里的断言目标不要只写“成功/失败”,要精确到错误码,这样自动化脚本才能区分是“校验逻辑错”还是“密码策略错”。锁定相关用例我在第六节再展开讲,因为它是典型的“用例之间互相影响”的场景。
3.2 支付金额实例:金额精度、限额与幂等
支付金额的需求描述看起来很简单:单笔支付金额必须是数字,允许0.01到50000.00元,最多两位小数。但这类用例最常翻车在精度处理上。先看这段校验逻辑:
from decimal import Decimal, InvalidOperation def validate_pay_amount(amount: str) -> tuple[bool, str]: # 用 Decimal 而不是 float,避免 0.1+0.2 不等于 0.3 的问题 try: raw = Decimal(amount) except InvalidOperation: return False, "金额格式非法" if raw != raw.quantize(Decimal("0.01")): return False, "金额最多两位小数" if raw < Decimal("0.01") or raw > Decimal("50000.00"): return False, "金额超出可支付范围" return True, "ok"参数说明:Decimal可以精确表达十进制小数,而float在二进制下无法精确表示0.1,金额场景必须用前者;quantize这一步判断的是“小数点后是否超过两位”。用例表设计如下:
| 用例编号 | 输入金额 | 预期结果 | 设计点 |
|---|---|---|---|
| TC-PAY-001 | 0.01 | 通过 | 下边界 |
| TC-PAY-002 | 50000.00 | 通过 | 上边界 |
| TC-PAY-003 | 0.00 | 拒绝 | 低于下边界 |
| TC-PAY-004 | 50000.01 | 拒绝 | 高于上边界 |
| TC-PAY-005 | 1.005 | 拒绝 | 三位小数 |
| TC-PAY-006 | 1.10 | 通过 | 两位小数有效 |
| TC-PAY-007 | abc | 拒绝 | 非数字 |
| TC-PAY-008 | 空字符串 | 拒绝 | 空输入 |
| TC-PAY-009 | -1.00 | 拒绝 | 负数 |
| TC-PAY-010 | 1e3 | 拒绝 | 科学计数法 |
TC-PAY-010是个容易被忽略的案例:Decimal能解析“1e3”,但支付接口一般不允许科学计数法。类似的还有前后空格、千分位逗号,这些格式是否允许要在用例评审时和产品确认,而不是想当然。支付金额还有一个经典场景是幂等:同一支付单号连续提交两次,第二次必须返回第一次的支付结果,不能重复扣款。这个用例和状态相关,需要放在场景法里设计,断言包括“两次响应中的支付单号一致”和“余额只扣减一次”。
3.3 搜索查询实例:空输入、字符长度与分页边界
搜索接口的需求是:关键词可为空,为空时返回热门推荐;关键词长度1到50个字符,大小写不敏感;多个关键词用空格分隔,连续多个空格视为一个;分页page从1开始,size默认20,最大50。核心逻辑:
def search(keyword: str | None, page: int = 1, size: int = 20) -> dict: # 空串和纯空格都算作“无关键词” if not keyword or not keyword.strip(): return {"items": hot_items(page, size), "total": len(hot_items)} if len(keyword) > 50: return {"code": 40010, "msg": "关键词长度不能超过50"} terms = keyword.strip().split() query = build_query(terms) return query.execute(page=page, size=min(size, 50))用例覆盖这些点:
| 用例编号 | 输入 | 预期 | 设计点 |
|---|---|---|---|
| TC-SEARCH-001 | None | 返回热门推荐 | 空值 |
| TC-SEARCH-002 | 空字符串 | 返回热门推荐 | 空字符串 |
| TC-SEARCH-003 | 三个空格 | 返回热门推荐 | 纯空白 |
| TC-SEARCH-004 | 单字符“测” | 正常结果 | 下边界 |
| TC-SEARCH-005 | 50个字符 | 正常结果 | 上边界内 |
| TC-SEARCH-006 | 51个字符 | 40010 | 上边界外 |
| TC-SEARCH-007 | 表情符号“🔍” | 按需求定义 | 字符计数 |
| TC-SEARCH-008 | “ABC”和“abc” | 结果一致 | 大小写 |
| TC-SEARCH-009 | “测试 开发”和“测试 开发” | 结果一致 | 连续空格归一 |
| TC-SEARCH-010 | size=0 | 拒绝或按1处理 | 分页下边界 |
| TC-SEARCH-011 | size=51 | 按50处理 | 分页上限 |
| TC-SEARCH-012 | page=0 | 拒绝或按1处理 | 页码边界 |
TC-SEARCH-007背后有个血泪经验:需求里写“50个字符”时,要问开发是按Unicode code point数还是按UTF-16 code unit数。中文和表情符号在这两种计数方式下长度不一样,测试时用“50个中文+1个表情”这样的输入去验证,比单纯数长度更能暴露问题。搜索这类接口的分页用例,还要额外确认size超过上限时是“报错”还是“静默截断”,两者都是合理设计,但用例预期必须和接口文档一致。
4. 测试用例设计避坑指南:四个从评审现场带回来的翻车案例
4.1 需求里的“或”被理解成“和”,等价类就分错了
现象:需求写“用户名可以是字母或数字”,某开发者设计用例时把“必须同时包含字母和数字”当成了预期,于是“abc123”这种按需求本该合法的输入,在他眼里变成了“无效等价类”,断言写错。幸好评审时产品当场指出来,否则上线就会误伤一批用户。
原因:自然语言里的“或/和”在需求文档里经常被写得不严谨,测试人员容易按自己的语义理解去划分等价类,等于先改了需求再写用例。
解决:每一条涉及枚举、范围、组合的规则,我都要求自己先改写成可判定的表达式。比如“字母或数字”写成[A-Za-z0-9]+,把“且”改成“同时满足”,然后和产品确认这个表达式是否符合意图。确认完再写进用例的“前置条件”里,这条用例以后就有了依据。
4.2 用例之间共享数据,导致失败用例重跑后通过
现象:锁定类用例在模拟项目X上运行时,“连续输错密码5次锁定”这条用例执行完后,紧接着的“正确密码登录成功”用例一直失败,但单独重跑后一条又能通过。反过来先跑“登录成功”再跑“锁定”,一切正常。
原因:两条用例共用同一个测试账号,第一条把账号的失败计数改成了5并触发锁定,第二条没有重新初始化账号状态。
解决:凡是被用例修改的状态数据,都要在用例执行前重置。我现在的做法是在测试套件的setup里给每个用例准备独立数据,用工厂函数创建账号,而不是在测试类开头只创建一个全局账号。代码里写成这样:
def setup_function(): # 每个测试函数前重建账号,避免失败计数、锁定状态跨用例污染 store = create_user_store("new_user", "Abcd1234") return store这条经验同样适用于优惠券(用过了还能不能用)、库存(扣减后余量变化)、验证码(错误次数累计)等一切有状态的功能。
4.3 用例写得像操作手册,接口一变全部重写
现象:一份用例里写着“输入用户名,点击登录按钮,在弹窗中点击确认,断言页面右上角出现绿色对勾”。后来UI改版,按钮从右上角移到左上角,弹窗变成了内嵌区域,这份用例全量失效。
原因:把“怎么操作”当成了“验证什么”,用例和UI实现绑得过紧。
解决:我写用例时只保留“前提条件”“操作步骤的业务意图”和“可判定的预期结果”。同样是登录成功这条用例,正确写法是“前提:使用未锁定的有效账号;操作:提交正确的用户名、密码和验证码;预期:接口返回code=0,会话建立”。页面上的具体位置、按钮颜色都不写进业务用例,它们属于UI自动化脚本要关心的事,脚本单独维护。
4.4 优先级全是P0,回归排期却只有半天
现象:某次发版前检查用例库,50条用例优先级全是P0,问就是“这条也可能出问题”“那条也不能不测”。最后回归排期只有半天,谁也跑不完,只能熬夜点鼠标。
原因:没有按影响面和发生概率给用例排序,优先级失去了筛选功能。
解决:我现在按“核心链路优先级最高”的原则分三级:登录、支付、下单主流程以及涉及资金安全的用例是P0;边界值、异常输入、权限校验是P1;文案、样式、埋点等低风险项是P2。回归时P0加P1必须全跑,P2按时间取舍。判断标准是:这条用例挂掉,用户是否能感知、是否会造成资金损失或数据损坏。
4.5 需求变更后用例没更新,回归用例成了摆设
现象:需求把验证码从4位改成6位,用例表里还是4位。后来线上出现5位验证码被服务器接受、但用例里没覆盖的情况,漏测原因就是用例没有跟着需求走。
原因:用例和需求之间没有建立追溯关系,需求变了没人知道要改哪些用例。
解决:每条用例的编号里带需求标识,比如REQ-AUTH-001对应的用例都写在同一段区域。需求变更评审时,负责测试的同学第一件事就是拉着“受影响的用例清单”过一遍,改完需求立即同步更新用例。这个习惯比任何工具都重要。
5. 进阶:把经典用例直接参数化成自动化用例,一份用例两处用
5.1 怎么把用例表转成pytest参数
手工用例和自动化用例之间最省力的桥梁是参数化。拿前面的登录用例举例,CASES列表里的每一行都对应手工用例表里的一行,代码可读性几乎没有损耗:
import pytest CASES = [ # username, password, captcha, expected_code ("abcd", "Abcd1234", "1234", 40001), # 用户名长度不足 ("abcdef", "Abcd1234", "1234", 0), # 正常登录 ("abcdef", "abcd1234", "1234", 40002), # 密码缺大写 ("abcdef", "Abcd1234", "0000", 40003), # 验证码错误 ("abcdef", "Wrong1", "1234", 40002), # 密码含非法字符 ] @pytest.mark.parametrize("username,password,captcha,expected_code", CASES) def test_login(username, password, captcha, expected_code): user_store = create_user_store("abcdef", "Abcd1234") captcha_store = create_captcha_store("abcdef", "1234") resp = login_check(username, password, captcha, user_store, captcha_store) assert resp["code"] == expected_code参数化之后,每一条数据在报告中都是一个独立用例,失败时能直接定位到是哪个输入组合挂了。CASES就是手工用例表的代码镜像,改用例时两边同步改,我一般把这段列表放在测试文件顶部加注释标明对应需求ID。如果公司习惯用Excel管理用例,还可以写个小脚本读CSV列生成这个列表,减少手工搬运。
5.2 什么时候不要自动化
参数化适合的是“输入输出可预测、前置状态可重置”的接口级用例。但也有不适合自动化的场景:一次性探索测试(新功能首轮试用)、强视觉断言(配色、布局、动画效果)、依赖第三方真实环境的端到端回归,这类场景保留手工用例表格反而更高效。我的原则是先问一句这条用例是否稳定可重复,如果每次跑的预期都会因为环境波动而变化,就不要自动化,否则自动化只会放大用例的脆弱性。
最后落一个个人习惯:写完一组用例后,我要求自己回答三个问题。第一,每条用例是不是都有明确的预期结果,能不能一句话说清“这个输入为什么被接受或拒绝”;第二,这条用例能不能重复执行,连续跑三次结果一致;第三,如果明天我不在,接手的人能不能只看用例表就在10分钟内跑通这套验证。这三个问题过完,用例基本不会踩前面那四个坑。这个习惯给我省过三次线上事故级的漏测,所以一直留到现在。希望帮到你。
本文还有配套的精品资源,点击获取