云计算百科
云计算领域专业知识百科平台

软件测试专栏(3/20):测试用例设计艺术:等价类、边界值、场景法实战

本文导读:如何用有限的测试用例发现无限的潜在缺陷?本文将揭秘三大经典测试用例设计方法——等价类划分、边界值分析、场景法,通过真实案例教你设计出高覆盖、高效率的测试用例。


一、从面试题到实战:一道题看出你的测试思维

“请设计一个手机号码输入框的测试用例”

初级测试的答案:

  • 输入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]之间的整数
无效等价类 不符合需求规格的输入数据集合 小于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字符)”

设计测试用例

“预期结果:有效类→应用成功无效类→对应错误提示”

测试用例表:

用例ID输入数据等价类类型预期结果
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_value1, min_value, min_value+1]:
    for j in [max_value1, 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
    用例是否可自动化?
    前置条件和后置条件是否明确?
    测试数据是否准备充分?

    六、常见误区与最佳实践

    常见误区:

  • 等价类划分不全:只考虑明显的情况,忽略特殊字符、编码等问题
  • 边界值只测正常值:忽略略大于/略小于边界的情况
  • 场景法变成功能列表:简单罗列功能点,而非真实用户场景
  • 过度设计:为不重要的功能设计大量用例,ROI低
  • 缺乏维护:用例设计后不再更新,逐渐失效
  • 最佳实践:

  • 组合使用:对核心功能综合使用三种方法
  • 风险驱动:高风险区域投入更多测试设计
  • 持续优化:根据缺陷分析调整用例设计策略
  • 团队评审:组织用例评审会,查漏补缺
  • 工具辅助:使用测试管理工具(如TestRail、Xray)管理用例
  • 测试用例设计质量指标:

    指标计算公式目标值
    需求覆盖率 已覆盖需求数 / 总需求数 ≥ 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篇干货更新!

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 软件测试专栏(3/20):测试用例设计艺术:等价类、边界值、场景法实战
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!