☰
测试用例设计实战:等价类、边界值与参数化在登录支付搜索场景的应用
2026/10/15 4:52:07 网站建设 项目流程

简介:一份适合软件测试初学者与测试工程师参考的用例设计实例文档,围绕黑盒测试、等价类划分、边界值分析、错误推测、因果图与判定表驱动等经典方法,给出可直接套用的用例模板与设计思路。文档先梳理了优秀用例应包含的十二项基本信息,再以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-00117拒绝下边界外
TC-AGE-00218通过下边界内
TC-AGE-00360通过上边界内
TC-AGE-00461拒绝上边界外
TC-AGE-00530通过等价类代表

这里有两个细节经常翻车。第一,如果需求写“大于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-001abcd(5位)Abcd1234123440001用户名下边界外
TC-LOGIN-002abcdef(6位)Abcd123412340用户名下边界内
TC-LOGIN-003abcdefghijklmnopqrst(20位)Abcd123412340用户名上边界内
TC-LOGIN-004长度21位的用户名Abcd1234123440001用户名上边界外
TC-LOGIN-005含中文的用户名Abcd1234123440001非法字符
TC-LOGIN-006abcdefAbcd123(7位)123440002密码下边界外
TC-LOGIN-007abcdefabcd1234(纯小写数字)123440002缺大写字母
TC-LOGIN-008abcdefAbcd1234567812340密码合法
TC-LOGIN-009abcdefAbcd1234123(3位)40003验证码位数不够
TC-LOGIN-010abcdefAbcd1234错误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-0010.01通过下边界
TC-PAY-00250000.00通过上边界
TC-PAY-0030.00拒绝低于下边界
TC-PAY-00450000.01拒绝高于上边界
TC-PAY-0051.005拒绝三位小数
TC-PAY-0061.10通过两位小数有效
TC-PAY-007abc拒绝非数字
TC-PAY-008空字符串拒绝空输入
TC-PAY-009-1.00拒绝负数
TC-PAY-0101e3拒绝科学计数法

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-001None返回热门推荐空值
TC-SEARCH-002空字符串返回热门推荐空字符串
TC-SEARCH-003三个空格返回热门推荐纯空白
TC-SEARCH-004单字符“测”正常结果下边界
TC-SEARCH-00550个字符正常结果上边界内
TC-SEARCH-00651个字符40010上边界外
TC-SEARCH-007表情符号“🔍”按需求定义字符计数
TC-SEARCH-008“ABC”和“abc”结果一致大小写
TC-SEARCH-009“测试 开发”和“测试 开发”结果一致连续空格归一
TC-SEARCH-010size=0拒绝或按1处理分页下边界
TC-SEARCH-011size=51按50处理分页上限
TC-SEARCH-012page=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分钟内跑通这套验证。这三个问题过完,用例基本不会踩前面那四个坑。这个习惯给我省过三次线上事故级的漏测,所以一直留到现在。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询