本文导读:如何用有限的测试用例发现无限的潜在缺陷?本文将揭秘三大经典测试用例设计方法——等价类划分、边界值分析、场景法,通过真实案例教你设计出高覆盖、高效率的测试用例。
一、从面试题到实战:一道题看出你的测试思维
“请设计一个手机号码输入框的测试用例”
初级测试的答案:
- 输入11位数字
- 输入少于11位数字
- 输入多于11位数字
- 输入带字母的字符串
- 输入特殊字符
高级测试的答案(基于方法论):
# 基于测试用例设计方法的系统化思考
class PhoneNumberTestCaseDesign:
def __init__(self):
self.methods = {
"等价类划分": self.equivalence_partitioning(),
"边界值分析": self.boundary_value_analysis(),
"场景法": self.scenario_based()
}
def equivalence_partitioning(self):
"""等价类划分法"""
return {
"有效等价类": [
"13开头的11位数字",
"14开头的11位数字",
"15开头的11位数字",
"16开头的11位数字",
"17开头的11位数字",
"18开头的11位数字",
"19开头的11位数字"
],
"无效等价类": [
"空输入",
"10位数字",
"12位数字",
"包含字母",
"包含特殊字符",
"包含中文",
"空格开头/结尾",
"全角数字",
"手机号段不存在(如120开头)"
]
}
def boundary_value_analysis(self):
"""边界值分析法"""
return {
"长度边界": [
"10位数字(长度-1)",
"11位数字(正常长度)",
"12位数字(长度+1)"
],
"数值边界": [
"13900000000(最小正常值附近)",
"13999999999(最大正常值附近)",
"13899999999(边界转换)"
]
}
def scenario_based(self):
"""场景法"""
return {
"正常场景": "用户注册时输入有效手机号",
"异常场景1": "已注册手机号重复输入",
"异常场景2": "输入后点击获取验证码多次",
"异常场景3": "粘贴复制手机号",
"异常场景4": "输入过程中删除部分字符",
"异常场景5": "网络中断后输入提交"
}
差距在哪里?
答案在于是否掌握了系统化的测试用例设计方法论。本文将带你掌握这些方法,让你的测试用例设计从"凭经验"到"凭科学"。
二、等价类划分法:化繁为简的艺术
2.1 核心思想
将无限的测试数据划分为有限的测试用例,每个等价类中的元素在测试中具有相同的行为特征。
2.2 基本概念
| 有效等价类 | 符合需求规格的输入数据集合 | [1, 120]之间的整数 |
| 无效等价类 | 不符合需求规格的输入数据集合 | 小于1的整数、大于120的整数、非数字字符等 |
| 弱组合 | 每个等价类选一个值测试 | 选30(有效)、0(无效)、abc(无效) |
| 强组合 | 不同等价类值组合测试 | 30+特殊字符、空值+边界值等组合 |
2.3 实战案例:电商优惠券系统
#mermaid-svg-bWVUeA1EO72r7j00{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-bWVUeA1EO72r7j00 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-bWVUeA1EO72r7j00 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-bWVUeA1EO72r7j00 .error-icon{fill:#552222;}#mermaid-svg-bWVUeA1EO72r7j00 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-bWVUeA1EO72r7j00 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-bWVUeA1EO72r7j00 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-bWVUeA1EO72r7j00 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-bWVUeA1EO72r7j00 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-bWVUeA1EO72r7j00 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-bWVUeA1EO72r7j00 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-bWVUeA1EO72r7j00 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-bWVUeA1EO72r7j00 .marker.cross{stroke:#333333;}#mermaid-svg-bWVUeA1EO72r7j00 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-bWVUeA1EO72r7j00 p{margin:0;}#mermaid-svg-bWVUeA1EO72r7j00 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-bWVUeA1EO72r7j00 .cluster-label text{fill:#333;}#mermaid-svg-bWVUeA1EO72r7j00 .cluster-label span{color:#333;}#mermaid-svg-bWVUeA1EO72r7j00 .cluster-label span p{background-color:transparent;}#mermaid-svg-bWVUeA1EO72r7j00 .label text,#mermaid-svg-bWVUeA1EO72r7j00 span{fill:#333;color:#333;}#mermaid-svg-bWVUeA1EO72r7j00 .node rect,#mermaid-svg-bWVUeA1EO72r7j00 .node circle,#mermaid-svg-bWVUeA1EO72r7j00 .node ellipse,#mermaid-svg-bWVUeA1EO72r7j00 .node polygon,#mermaid-svg-bWVUeA1EO72r7j00 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-bWVUeA1EO72r7j00 .rough-node .label text,#mermaid-svg-bWVUeA1EO72r7j00 .node .label text,#mermaid-svg-bWVUeA1EO72r7j00 .image-shape .label,#mermaid-svg-bWVUeA1EO72r7j00 .icon-shape .label{text-anchor:middle;}#mermaid-svg-bWVUeA1EO72r7j00 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-bWVUeA1EO72r7j00 .rough-node .label,#mermaid-svg-bWVUeA1EO72r7j00 .node .label,#mermaid-svg-bWVUeA1EO72r7j00 .image-shape .label,#mermaid-svg-bWVUeA1EO72r7j00 .icon-shape .label{text-align:center;}#mermaid-svg-bWVUeA1EO72r7j00 .node.clickable{cursor:pointer;}#mermaid-svg-bWVUeA1EO72r7j00 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-bWVUeA1EO72r7j00 .arrowheadPath{fill:#333333;}#mermaid-svg-bWVUeA1EO72r7j00 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-bWVUeA1EO72r7j00 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-bWVUeA1EO72r7j00 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bWVUeA1EO72r7j00 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-bWVUeA1EO72r7j00 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bWVUeA1EO72r7j00 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-bWVUeA1EO72r7j00 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-bWVUeA1EO72r7j00 .cluster text{fill:#333;}#mermaid-svg-bWVUeA1EO72r7j00 .cluster span{color:#333;}#mermaid-svg-bWVUeA1EO72r7j00 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-bWVUeA1EO72r7j00 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-bWVUeA1EO72r7j00 rect.text{fill:none;stroke-width:0;}#mermaid-svg-bWVUeA1EO72r7j00 .icon-shape,#mermaid-svg-bWVUeA1EO72r7j00 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bWVUeA1EO72r7j00 .icon-shape p,#mermaid-svg-bWVUeA1EO72r7j00 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-bWVUeA1EO72r7j00 .icon-shape rect,#mermaid-svg-bWVUeA1EO72r7j00 .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bWVUeA1EO72r7j00 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-bWVUeA1EO72r7j00 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-bWVUeA1EO72r7j00 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
优惠券输入框测试
等价类划分
有效等价类
无效等价类
“格式正确且有效如:SAVE50”
“不区分大小写如:save50”
“包含数字和字母如:DISCOUNT20”
“格式错误如:SAVE_50(含特殊字符)”
“已过期优惠码”
“已达使用上限”
“不存在的优惠码”
“空字符串”
“超过长度限制(如>20字符)”
设计测试用例
“预期结果:有效类→应用成功无效类→对应错误提示”
测试用例表:
| TC-EC-01 | SAVE50 | 有效等价类 | 优惠券应用成功,价格更新 |
| TC-EC-02 | save50 | 有效等价类 | 优惠券应用成功(不区分大小写) |
| TC-EC-03 | DISCOUNT20 | 有效等价类 | 优惠券应用成功 |
| TC-EC-04 | SAVE_50 | 无效等价类 | 提示"优惠码格式错误" |
| TC-EC-05 | EXPIRED01 | 无效等价类 | 提示"优惠券已过期" |
| TC-EC-06 | LIMITREACH | 无效等价类 | 提示"已达使用次数上限" |
| TC-EC-07 | NOTEXIST | 无效等价类 | 提示"优惠券不存在" |
| TC-EC-08 | (空) | 无效等价类 | 提示"请输入优惠码" |
| TC-EC-09 | A*50字符超长 | 无效等价类 | 输入框限制20字符或提示超长 |
2.4 等价类划分的六大原则
三、边界值分析法:缺陷高发区的精准打击
3.1 为什么边界值如此重要?
研究表明,超过70%的软件缺陷发生在边界条件附近。边界值分析就是针对这些"高危区域"的精准测试。
3.2 边界值的三种类型
# 边界值分析的Python实现示例
def analyze_boundaries(min_value, max_value):
"""分析边界值"""
# 1. 基本边界值(最常用)
basic_boundaries = {
"最小值": min_value,
"略高于最小值": min_value + 1,
"略低于最小值": min_value – 1,
"最大值": max_value,
"略低于最大值": max_value – 1,
"略高于最大值": max_value + 1,
"典型中间值": (min_value + max_value) // 2
}
# 2. 健壮性边界值(考虑异常)
robust_boundaries = basic_boundaries.copy()
robust_boundaries.update({
"远小于最小值": min_value – 10, # 超出范围
"远大于最大值": max_value + 10, # 超出范围
"零值": 0 if min_value <= 0 <= max_value else "N/A",
"负值": –1 if min_value > 0 else "N/A"
})
# 3. 最坏情况边界值(组合边界)
worst_case = []
for i in [min_value–1, min_value, min_value+1]:
for j in [max_value–1, max_value, max_value+1]:
worst_case.append((i, j))
return {
"基本边界": basic_boundaries,
"健壮边界": robust_boundaries,
"最坏情况": worst_case[:5] # 取前5个示例
}
# 示例:年龄输入框 1-120岁
boundaries = analyze_boundaries(1, 120)
print("年龄输入框边界值分析:")
for category, values in boundaries.items():
print(f"\\n{category}:")
for desc, value in (values.items() if isinstance(values, dict) else enumerate(values)):
print(f" {desc}: {value}")
3.3 实战案例:文件上传功能
需求:支持上传1MB到100MB的文件,格式为jpg、png、pdf
#mermaid-svg-FwPfgzEhdoX9ao81{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-FwPfgzEhdoX9ao81 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-FwPfgzEhdoX9ao81 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-FwPfgzEhdoX9ao81 .error-icon{fill:#552222;}#mermaid-svg-FwPfgzEhdoX9ao81 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-FwPfgzEhdoX9ao81 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-FwPfgzEhdoX9ao81 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-FwPfgzEhdoX9ao81 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-FwPfgzEhdoX9ao81 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-FwPfgzEhdoX9ao81 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-FwPfgzEhdoX9ao81 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-FwPfgzEhdoX9ao81 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-FwPfgzEhdoX9ao81 .marker.cross{stroke:#333333;}#mermaid-svg-FwPfgzEhdoX9ao81 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-FwPfgzEhdoX9ao81 p{margin:0;}#mermaid-svg-FwPfgzEhdoX9ao81 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-FwPfgzEhdoX9ao81 .cluster-label text{fill:#333;}#mermaid-svg-FwPfgzEhdoX9ao81 .cluster-label span{color:#333;}#mermaid-svg-FwPfgzEhdoX9ao81 .cluster-label span p{background-color:transparent;}#mermaid-svg-FwPfgzEhdoX9ao81 .label text,#mermaid-svg-FwPfgzEhdoX9ao81 span{fill:#333;color:#333;}#mermaid-svg-FwPfgzEhdoX9ao81 .node rect,#mermaid-svg-FwPfgzEhdoX9ao81 .node circle,#mermaid-svg-FwPfgzEhdoX9ao81 .node ellipse,#mermaid-svg-FwPfgzEhdoX9ao81 .node polygon,#mermaid-svg-FwPfgzEhdoX9ao81 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-FwPfgzEhdoX9ao81 .rough-node .label text,#mermaid-svg-FwPfgzEhdoX9ao81 .node .label text,#mermaid-svg-FwPfgzEhdoX9ao81 .image-shape .label,#mermaid-svg-FwPfgzEhdoX9ao81 .icon-shape .label{text-anchor:middle;}#mermaid-svg-FwPfgzEhdoX9ao81 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-FwPfgzEhdoX9ao81 .rough-node .label,#mermaid-svg-FwPfgzEhdoX9ao81 .node .label,#mermaid-svg-FwPfgzEhdoX9ao81 .image-shape .label,#mermaid-svg-FwPfgzEhdoX9ao81 .icon-shape .label{text-align:center;}#mermaid-svg-FwPfgzEhdoX9ao81 .node.clickable{cursor:pointer;}#mermaid-svg-FwPfgzEhdoX9ao81 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-FwPfgzEhdoX9ao81 .arrowheadPath{fill:#333333;}#mermaid-svg-FwPfgzEhdoX9ao81 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-FwPfgzEhdoX9ao81 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-FwPfgzEhdoX9ao81 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FwPfgzEhdoX9ao81 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-FwPfgzEhdoX9ao81 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FwPfgzEhdoX9ao81 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-FwPfgzEhdoX9ao81 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-FwPfgzEhdoX9ao81 .cluster text{fill:#333;}#mermaid-svg-FwPfgzEhdoX9ao81 .cluster span{color:#333;}#mermaid-svg-FwPfgzEhdoX9ao81 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-FwPfgzEhdoX9ao81 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-FwPfgzEhdoX9ao81 rect.text{fill:none;stroke-width:0;}#mermaid-svg-FwPfgzEhdoX9ao81 .icon-shape,#mermaid-svg-FwPfgzEhdoX9ao81 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FwPfgzEhdoX9ao81 .icon-shape p,#mermaid-svg-FwPfgzEhdoX9ao81 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-FwPfgzEhdoX9ao81 .icon-shape rect,#mermaid-svg-FwPfgzEhdoX9ao81 .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FwPfgzEhdoX9ao81 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-FwPfgzEhdoX9ao81 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-FwPfgzEhdoX9ao81 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
文件上传测试
“大小边界1MB-100MB”
“格式边界jpg/png/pdf”
“最小值边界: 1MB”
“略小于最小值: 0.9MB”
“略大于最小值: 1.1MB”
“最大值边界: 100MB”
“略小于最大值: 99MB”
“略大于最大值: 101MB”
“典型值: 50MB”
“空文件: 0MB”
“超大文件: 200MB”
“有效格式: jpg”
“有效格式: png”
“有效格式: pdf”
“边界格式: JPG(大写)”
“边界格式: jPg(大小写混合)”
“无效格式: txt”
“无效格式: exe”
“无格式: 无后缀名”
“错误格式: jpg.txt(双重后缀)”
预期结果验证
边界值测试用例表:
| TC-BV-01 | 1MB | test.jpg | 上传成功 | 最小值处理 |
| TC-BV-02 | 0.9MB | test.jpg | 提示"文件太小" | 下限边界 |
| TC-BV-03 | 100MB | test.pdf | 上传成功 | 最大值处理 |
| TC-BV-04 | 101MB | test.png | 提示"文件太大" | 上限边界 |
| TC-BV-05 | 0MB | test.jpg | 提示"文件为空" | 零值处理 |
| TC-BV-06 | 50MB | test.JPG | 上传成功 | 格式大小写 |
| TC-BV-07 | 1MB | test.txt | 提示"格式不支持" | 格式边界 |
| TC-BV-08 | 100MB | (无后缀) | 提示"格式不支持" | 无格式 |
| TC-BV-09 | 1MB | test.jpg.txt | 提示"格式不支持" | 双重后缀 |
3.4 边界值分析的高级技巧
技巧1:内部边界值(适用于数组、列表)
# 数组索引边界测试
def test_array_boundaries():
items = ["A", "B", "C", "D", "E"] # 5个元素
test_cases = [
{"index": 0, "desc": "第一个元素", "expected": "A"},
{"index": 4, "desc": "最后一个元素", "expected": "E"},
{"index": 2, "desc": "中间元素", "expected": "C"},
{"index": –1, "desc": "负索引", "expected": "E"}, # Python特性
{"index": 5, "desc": "越界索引", "expected": "IndexError"},
{"index": –6, "desc": "负越界", "expected": "IndexError"},
]
return test_cases
技巧2:时间边界值
# 时间相关边界测试
def test_time_boundaries():
# 假设系统处理时间范围:9:00-18:00
test_cases = [
{"time": "09:00:00", "desc": "开始时间", "expected": "可处理"},
{"time": "08:59:59", "desc": "开始前1秒", "expected": "不可处理"},
{"time": "18:00:00", "desc": "结束时间", "expected": "可处理"},
{"time": "18:00:01", "desc": "结束后1秒", "expected": "不可处理"},
{"time": "12:00:00", "desc": "中间时间", "expected": "可处理"},
{"time": "00:00:00", "desc": "午夜", "expected": "不可处理"},
{"time": "23:59:59", "desc": "午夜前", "expected": "不可处理"},
]
# 日期边界
date_cases = [
{"date": "2023-02-28", "desc": "2月最后一天"},
{"date": "2024-02-29", "desc": "闰年2月29日"}, # 特殊边界
{"date": "2023-12-31", "desc": "年底"},
{"date": "2024-01-01", "desc": "年初"},
{"date": "2023-06-30", "desc": "半年末"},
]
return test_cases, date_cases
四、场景法:用户故事的真实还原
4.1 什么是场景法?
通过模拟真实用户使用场景,设计基于业务流程的测试用例。特别适合复杂业务流程的测试。
4.2 场景法的核心:基本流和备选流
#mermaid-svg-bqO0Aeiy0MgHm4Bf{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-bqO0Aeiy0MgHm4Bf .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-bqO0Aeiy0MgHm4Bf .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-bqO0Aeiy0MgHm4Bf .error-icon{fill:#552222;}#mermaid-svg-bqO0Aeiy0MgHm4Bf .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-bqO0Aeiy0MgHm4Bf .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-bqO0Aeiy0MgHm4Bf .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-bqO0Aeiy0MgHm4Bf .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-bqO0Aeiy0MgHm4Bf .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-bqO0Aeiy0MgHm4Bf .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-bqO0Aeiy0MgHm4Bf .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-bqO0Aeiy0MgHm4Bf .marker{fill:#333333;stroke:#333333;}#mermaid-svg-bqO0Aeiy0MgHm4Bf .marker.cross{stroke:#333333;}#mermaid-svg-bqO0Aeiy0MgHm4Bf svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-bqO0Aeiy0MgHm4Bf p{margin:0;}#mermaid-svg-bqO0Aeiy0MgHm4Bf .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-bqO0Aeiy0MgHm4Bf .cluster-label text{fill:#333;}#mermaid-svg-bqO0Aeiy0MgHm4Bf .cluster-label span{color:#333;}#mermaid-svg-bqO0Aeiy0MgHm4Bf .cluster-label span p{background-color:transparent;}#mermaid-svg-bqO0Aeiy0MgHm4Bf .label text,#mermaid-svg-bqO0Aeiy0MgHm4Bf span{fill:#333;color:#333;}#mermaid-svg-bqO0Aeiy0MgHm4Bf .node rect,#mermaid-svg-bqO0Aeiy0MgHm4Bf .node circle,#mermaid-svg-bqO0Aeiy0MgHm4Bf .node ellipse,#mermaid-svg-bqO0Aeiy0MgHm4Bf .node polygon,#mermaid-svg-bqO0Aeiy0MgHm4Bf .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-bqO0Aeiy0MgHm4Bf .rough-node .label text,#mermaid-svg-bqO0Aeiy0MgHm4Bf .node .label text,#mermaid-svg-bqO0Aeiy0MgHm4Bf .image-shape .label,#mermaid-svg-bqO0Aeiy0MgHm4Bf .icon-shape .label{text-anchor:middle;}#mermaid-svg-bqO0Aeiy0MgHm4Bf .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-bqO0Aeiy0MgHm4Bf .rough-node .label,#mermaid-svg-bqO0Aeiy0MgHm4Bf .node .label,#mermaid-svg-bqO0Aeiy0MgHm4Bf .image-shape .label,#mermaid-svg-bqO0Aeiy0MgHm4Bf .icon-shape .label{text-align:center;}#mermaid-svg-bqO0Aeiy0MgHm4Bf .node.clickable{cursor:pointer;}#mermaid-svg-bqO0Aeiy0MgHm4Bf .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-bqO0Aeiy0MgHm4Bf .arrowheadPath{fill:#333333;}#mermaid-svg-bqO0Aeiy0MgHm4Bf .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-bqO0Aeiy0MgHm4Bf .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-bqO0Aeiy0MgHm4Bf .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bqO0Aeiy0MgHm4Bf .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-bqO0Aeiy0MgHm4Bf .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bqO0Aeiy0MgHm4Bf .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-bqO0Aeiy0MgHm4Bf .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-bqO0Aeiy0MgHm4Bf .cluster text{fill:#333;}#mermaid-svg-bqO0Aeiy0MgHm4Bf .cluster span{color:#333;}#mermaid-svg-bqO0Aeiy0MgHm4Bf div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-bqO0Aeiy0MgHm4Bf .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-bqO0Aeiy0MgHm4Bf rect.text{fill:none;stroke-width:0;}#mermaid-svg-bqO0Aeiy0MgHm4Bf .icon-shape,#mermaid-svg-bqO0Aeiy0MgHm4Bf .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bqO0Aeiy0MgHm4Bf .icon-shape p,#mermaid-svg-bqO0Aeiy0MgHm4Bf .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-bqO0Aeiy0MgHm4Bf .icon-shape rect,#mermaid-svg-bqO0Aeiy0MgHm4Bf .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bqO0Aeiy0MgHm4Bf .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-bqO0Aeiy0MgHm4Bf .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-bqO0Aeiy0MgHm4Bf :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
是
否
是
否
“开始:用户登录系统”
“基本流:1. 输入正确账号密码2. 点击登录3. 进入首页”
“是否有商品?”
“基本流续:4. 浏览商品5. 加入购物车6. 下单支付7. 支付成功”
“备选流1:4. 显示无商品5. 返回首页”
“库存是否足够?”
“基本流结束:8. 生成订单9. 显示成功”
“备选流2:8. 提示库存不足9. 返回购物车”
“备选流3:1. 输入错误密码2. 提示密码错误3. 返回登录页”
“备选流4:1. 账号被锁定2. 提示账号异常3. 联系客服”
“备选流5:7. 支付失败8. 提示失败原因9. 重试支付”
4.3 实战案例:电商订单完整流程
场景描述:用户从浏览商品到支付完成的完整流程
class ECommerceScenario:
"""电商订单场景测试设计"""
def __init__(self):
self.scenarios = {
"happy_path": self.happy_path(),
"alternative_paths": self.alternative_paths(),
"exception_paths": self.exception_paths()
}
def happy_path(self):
"""基本流/快乐路径"""
return [
{
"step": 1,
"action": "用户登录成功",
"data": {"username": "test_user", "password": "correct_pwd"},
"expected": "跳转至首页"
},
{
"step": 2,
"action": "搜索商品",
"data": {"keyword": "iPhone 15"},
"expected": "显示搜索结果列表"
},
{
"step": 3,
"action": "选择商品进入详情页",
"data": {"product_id": "P1001"},
"expected": "显示商品详情、价格、库存"
},
{
"step": 4,
"action": "加入购物车",
"data": {"quantity": 1},
"expected": "购物车数量+1,显示添加成功"
},
{
"step": 5,
"action": "进入购物车结算",
"data": {},
"expected": "显示订单确认页面"
},
{
"step": 6,
"action": "选择收货地址",
"data": {"address_id": "ADDR001"},
"expected": "地址选择成功"
},
{
"step": 7,
"action": "选择支付方式",
"data": {"payment": "alipay"},
"expected": "跳转支付页面"
},
{
"step": 8,
"action": "支付成功",
"data": {"amount": "6999.00"},
"expected": "显示支付成功,生成订单"
}
]
def alternative_paths(self):
"""备选流"""
return {
"修改商品数量": [
{"action": "购物车中修改数量为2", "expected": "总价自动更新"},
{"action": "修改数量为0", "expected": "提示数量至少为1"}
],
"更换收货地址": [
{"action": "选择其他收货地址", "expected": "地址更新成功"},
{"action": "新增收货地址", "expected": "可成功添加并选择"}
],
"更换支付方式": [
{"action": "更换为微信支付", "expected": "跳转微信支付"},
{"action": "更换为货到付款", "expected": "直接生成订单"}
],
"使用优惠券": [
{"action": "应用有效优惠券", "expected": "价格减少"},
{"action": "应用过期优惠券", "expected": "提示优惠券无效"}
]
}
def exception_paths(self):
"""异常流"""
return {
"登录异常": [
{"action": "错误密码登录", "expected": "提示密码错误"},
{"action": "不存在的用户登录", "expected": "提示用户不存在"},
{"action": "账号被锁定登录", "expected": "提示账号异常"}
],
"商品异常": [
{"action": "商品已下架", "expected": "提示商品不可购买"},
{"action": "库存不足", "expected": "提示库存不足"},
{"action": "商品价格变更", "expected": "提示价格已更新"}
],
"订单异常": [
{"action": "重复提交订单", "expected": "提示订单已存在"},
{"action": "超时未支付", "expected": "订单自动取消"},
{"action": "并发下单", "expected": "库存正确扣减"}
],
"支付异常": [
{"action": "支付失败", "expected": "提示支付失败原因"},
{"action": "支付中途取消", "expected": "返回订单确认页"},
{"action": "重复支付", "expected": "提示已支付"}
]
}
def generate_test_cases(self):
"""生成场景测试用例"""
test_cases = []
# 基本流测试用例
test_cases.append({
"id": "SC-HAPPY-001",
"name": "完整购物流程_基本流",
"scenario": "新用户首次完成购物全流程",
"steps": self.happy_path(),
"priority": "P0"
})
# 备选流组合测试用例
test_cases.append({
"id": "SC-ALT-001",
"name": "使用优惠券购物流程",
"scenario": "用户在购物过程中使用优惠券",
"steps": [
*self.happy_path()[:5], # 前5步相同
{
"step": 6,
"action": "应用优惠券DISCOUNT100",
"expected": "总价减少100元"
},
*self.happy_path()[6:] # 后续步骤
],
"priority": "P1"
})
# 异常流测试用例
for category, exceptions in self.exception_paths().items():
for i, exception in enumerate(exceptions, 1):
test_cases.append({
"id": f"SC-EXCEPT-{category.upper()}–{i:03d}",
"name": f"{category}_{exception['action']}",
"scenario": f"测试{exception['action']}场景",
"steps": [
*self.happy_path()[:3], # 到商品详情页
{
"step": 4,
"action": exception['action'],
"expected": exception['expected']
}
],
"priority": "P2"
})
return test_cases
4.4 场景法设计模板
## 测试场景:[场景名称]
### 场景描述
[简要描述该场景的业务背景]
### 参与角色
– 主要角色:[如:普通用户、管理员]
– 次要角色:[如:客服、系统]
### 前置条件
1. [条件1]
2. [条件2]
3. [条件3]
### 基本流(主成功场景)
| 步骤 | 用户操作 | 系统响应 | 测试数据 |
|——|———-|———-|———-|
| 1 | [操作描述] | [期望响应] | [测试数据] |
| 2 | [操作描述] | [期望响应] | [测试数据] |
| 3 | [操作描述] | [期望响应] | [测试数据] |
### 备选流(扩展场景)
#### 备选流A:[场景名称]
– **触发条件**:[何时触发]
– **流程变化**:[与基本流的差异]
– **预期结果**:[系统响应]
#### 备选流B:[场景名称]
– **触发条件**:[何时触发]
– **流程变化**:[与基本流的差异]
– **预期结果**:[系统响应]
### 异常流(错误场景)
#### 异常1:[错误类型]
– **错误条件**:[何时发生错误]
– **系统响应**:[错误处理方式]
– **恢复机制**:[如何恢复正常]
### 后置条件
– [场景结束后系统状态]
– [数据清理要求]
### 测试用例
| 用例ID | 测试场景 | 优先级 | 是否自动化 |
|——–|———-|——–|————|
| TC-[ID] | [场景描述] | P[0-2] | [是/否] |
五、综合实战:三大方法的融合应用
案例:银行转账功能测试设计
需求:用户可通过网银进行转账,单笔限额1-50000元,每日限额100000元
class BankTransferTestDesign:
"""银行转账功能综合测试设计"""
def design_test_cases(self):
"""综合应用三大方法设计测试用例"""
test_cases = []
# 1. 等价类划分:转账金额
amount_equivalence_classes = {
"有效等价类": [
{"desc": "最小可转金额", "value": 1},
{"desc": "正常转账金额", "value": 1000},
{"desc": "最大可转金额", "value": 50000}
],
"无效等价类": [
{"desc": "金额为0", "value": 0},
{"desc": "金额为负", "value": –100},
{"desc": "超过单笔限额", "value": 50001},
{"desc": "非数字金额", "value": "abc"},
{"desc": "金额为空", "value": ""},
{"desc": "小数金额", "value": 100.50}, # 根据需求:可能只支持整数
{"desc": "科学计数法", "value": "1e3"}
]
}
# 2. 边界值分析:金额边界
amount_boundaries = [
0, # 最小值-1
1, # 最小值
2, # 最小值+1
49999, # 最大值-1
50000, # 最大值
50001 # 最大值+1
]
# 3. 场景法:转账流程
transfer_scenarios = {
"基本流": self.happy_path(),
"余额不足": self.insufficient_balance(),
"超过日限额": self.daily_limit_exceeded(),
"收款人不存在": self.payee_not_exist(),
"网络超时": self.network_timeout()
}
# 生成综合测试用例
case_id = 1
# 等价类测试用例
for class_type, cases in amount_equivalence_classes.items():
for case in cases:
test_cases.append({
"id": f"TT-EQ-{case_id:03d}",
"method": "等价类划分",
"场景": f"转账金额{case['desc']}",
"测试数据": {"amount": case['value']},
"优先级": "P1" if class_type == "有效等价类" else "P2"
})
case_id += 1
# 边界值测试用例
for amount in amount_boundaries:
test_cases.append({
"id": f"TT-BV-{case_id:03d}",
"method": "边界值分析",
"场景": f"转账金额边界测试:{amount}元",
"测试数据": {"amount": amount},
"优先级": "P0"
})
case_id += 1
# 场景测试用例
for scenario_name, steps in transfer_scenarios.items():
test_cases.append({
"id": f"TT-SC-{case_id:03d}",
"method": "场景法",
"场景": f"转账{scenario_name}场景",
"步骤": steps,
"优先级": "P0" if scenario_name == "基本流" else "P1"
})
case_id += 1
return test_cases
def happy_path(self):
"""基本流:正常转账成功"""
return [
"用户登录网银系统",
"进入转账页面",
"选择转账类型(行内/跨行)",
"输入收款人信息(账户、姓名)",
"输入转账金额(1000元)",
"输入支付密码",
"确认转账信息",
"系统扣款并显示成功",
"生成转账记录"
]
测试用例设计检查清单:
| 是否覆盖所有有效等价类? | ☐ | |
| 是否覆盖边界值(min, min+1, max-1, max)? | ☐ | |
| 是否考虑无效等价类? | ☐ | |
| 是否设计基本流(快乐路径)? | ☐ | |
| 是否考虑主要备选流? | ☐ | |
| 是否考虑异常场景? | ☐ | |
| 用例优先级是否合理? | ☐ | P0>P1>P2 |
| 用例是否可自动化? | ☐ | |
| 前置条件和后置条件是否明确? | ☐ | |
| 测试数据是否准备充分? | ☐ |
六、常见误区与最佳实践
常见误区:
最佳实践:
测试用例设计质量指标:
| 需求覆盖率 | 已覆盖需求数 / 总需求数 | ≥ 95% |
| 等价类覆盖率 | 已覆盖等价类数 / 总等价类数 | 100% |
| 边界值覆盖率 | 已测试边界值 / 总边界值 | 100% |
| 场景覆盖率 | 已覆盖场景数 / 总场景数 | ≥ 90% |
| 缺陷发现率 | 用例发现的缺陷数 / 总缺陷数 | ≥ 70% |
| 用例有效性 | 发现缺陷的用例数 / 总用例数 | ≥ 30% |
七、结语:从方法到思维
掌握等价类划分、边界值分析、场景法,不仅仅是学会了三种技术,更是培养了系统化的测试思维:
下一篇文章预告:缺陷的艺术:Bug从发现到关闭的全流程管理
附录:测试用例设计模板库
模板1:功能测试用例模板
## [功能模块]测试用例
### 基本信息
– 功能模块:[模块名称]
– 需求ID:[需求编号]
– 设计人员:[姓名]
– 设计日期:[YYYY-MM-DD]
### 测试用例
| 用例ID | 用例标题 | 前置条件 | 测试步骤 | 测试数据 | 预期结果 | 优先级 | 是否自动化 |
|——–|———-|———-|———-|———-|———-|——–|————|
| | | | | | | | |
模板2:场景测试用例模板
## [场景名称]场景测试
### 场景图
```mermaid
[场景流程图]
测试矩阵
| 基本流 | ||||
| 备选流 | ||||
| 异常流 |
### 模板3:API测试用例模板
```python
# API测试用例模板
api_test_cases = [
{
"case_id": "API-001",
"name": "正常请求测试",
"endpoint": "/api/v1/users",
"method": "POST",
"headers": {"Content-Type": "application/json"},
"request_body": {
"username": "test_user",
"email": "test@example.com"
},
"expected_status": 201,
"expected_response": {
"id": "<generated_id>",
"username": "test_user"
},
"validation_rules": [
"response包含id字段",
"status_code为201",
"username与请求一致"
]
}
]
欢迎在评论区交流:
点赞 + 收藏 + 关注,不错过后续17篇干货更新!
网硕互联帮助中心
评论前必须登录!
注册