Python实战:从三角形判断代码到白盒测试用例设计

记得刚开始学软件测试时,老师总让我们死记硬背各种测试方法,却很少解释这些理论在实际项目中怎么用。直到有次实习,mentor让我给一个简单的三角形判断函数写测试用例,我才真正理解白盒测试的价值。今天,我们就用Python重走这段学习之路,不再枯燥地背概念,而是动手写代码、画流程、设计用例,体验完整的测试思维训练。

1. 三角形判断的Python实现

我们先从一个完整的三角形判断函数开始。与传统的C语言实现不同,这里我们用Python的现代语法特性,让代码更简洁易读:

def classify_triangle(a, b, c):
    # 验证输入有效性
    if not all(isinstance(side, (int, float)) for side in [a, b, c]):
        raise ValueError("边长必须为数字")
    if any(side <= 0 for side in [a, b, c]):
        raise ValueError("边长必须大于0")
    
    # 排序确保a <= b <= c
    a, b, c = sorted([a, b, c])
    
    # 三角形不等式验证
    if a + b <= c:
        return "非三角形"
    
    # 类型判断
    if a == b == c:
        return "等边三角形"
    if a == b or b == c:
        return "等腰三角形"
    return "一般三角形"

这个版本有几个关键改进:

  • 增加了输入验证,处理非数字和零/负数的边界情况
  • 使用Python内置的sorted替代手工实现的冒泡排序
  • 采用更Pythonic的条件表达式
  • 明确的返回值而非直接打印,便于测试

核心逻辑流程图:

开始 → 输入验证 → 边长排序 → 三角形验证 → 类型判断 → 返回结果
                ↘ 无效输入异常      ↘ 非三角形

2. 逆向推导控制流程图

白盒测试的核心是理解代码的执行路径。让我们从上面的Python代码反向推导出详细的流程图:

  1. 输入验证分支:

    • 路径1:输入包含非数字 → 抛出ValueError
    • 路径2:输入含≤0的值 → 抛出ValueError
    • 路径3:输入有效 → 进入排序
  2. 三角形验证分支:

    • 路径4:a + b ≤ c → 返回"非三角形"
    • 路径5:a + b > c → 进入类型判断
  3. 类型判断分支:

    • 路径6:a == b == c → 返回"等边"
    • 路径7:a == b或b == c → 返回"等腰"
    • 路径8:其他情况 → 返回"一般"

用表格更清晰地展示路径覆盖:

路径编号覆盖条件预期输出
1输入非数字ValueError
2输入≤0的值ValueError
3→5→6有效输入且a=b=c等边三角形
3→5→7有效输入且两边相等等腰三角形
3→5→8有效输入且三边不等一般三角形
3→4a+b≤c非三角形

3. 使用pytest设计测试用例

现在,我们基于流程图设计具体的测试用例。Python的pytest框架比传统的assert更强大:

import pytest

@pytest.mark.parametrize("a,b,c,expected", [
    # 无效输入测试
    ("3", 4, 5, pytest.raises(ValueError)),
    (0, 1, 2, pytest.raises(ValueError)),
    (-1, 2, 3, pytest.raises(ValueError)),
    
    # 有效输入测试
    (1, 1, 1, "等边三角形"),  # 路径3→5→6
    (2, 2, 3, "等腰三角形"),  # 路径3→5→7
    (3, 4, 5, "一般三角形"),  # 路径3→5→8
    (1, 2, 4, "非三角形"),    # 路径3→4
    
    # 边界值测试
    (1, 1, 1.0, "等边三角形"),  # 整数与浮点数混合
    (1e6, 1e6, 1e6, "等边三角形"),  # 大数
    (0.1, 0.1, 0.1, "等边三角形"),  # 小数
])
def test_classify_triangle(a, b, c, expected):
    if isinstance(expected, type) and issubclass(expected, Exception):
        with expected:
            classify_triangle(a, b, c)
    else:
        assert classify_triangle(a, b, c) == expected

这个测试套件有几个亮点:

  • 使用@parametrize批量组织测试用例
  • 同时测试正常路径和异常路径
  • 包含边界值测试案例
  • 支持异常捕获的优雅写法

4. 测试覆盖率分析与改进

仅仅通过测试用例还不够,我们需要量化覆盖率。使用pytest-cov插件:

pytest --cov=your_module test_triangle.py

典型的覆盖率提升过程:

  1. 初始覆盖率报告:

    Name             Stmts   Miss  Cover
    -----------------------------------
    triangle.py        15      2    87%
    
  2. 分析缺失覆盖:

    • 可能遗漏了某些类型的等腰判断(如a=c但a≠b的情况)
    • 未测试浮点数比较的精度问题
  3. 补充测试用例:

    # 在parametrize中添加
    (2.5, 2.5, 3.0, "等腰三角形"),
    (3, 4, 3, "等腰三角形"),  # a=c
    (4, 3, 3, "等腰三角形"),  # b=c
    (1, 1, 1.0000001, "等边三角形"),  # 浮点精度
    
  4. 最终覆盖率:

    Name             Stmts   Miss  Cover
    -----------------------------------
    triangle.py        15      0   100%
    

覆盖率优化技巧:

  • 使用pytest --cov-report=html生成可视化报告
  • 特别注意边界条件(如a + b == c的临界情况)
  • 检查所有异常分支是否被覆盖
  • 对排序逻辑添加专门测试

5. 白盒测试进阶技巧

掌握了基础路径覆盖后,我们可以进一步提升测试质量:

条件组合测试: 对于复杂的条件判断(如a == b or b == c),应考虑所有可能的组合:

a == bb == c结果
TrueTrue等边
TrueFalse等腰
FalseTrue等腰
FalseFalse一般/非三角形

循环测试策略: 虽然当前代码没有循环,但如果有(如多次输入验证),应测试:

  • 零次循环
  • 一次循环
  • 多次循环
  • 循环中断条件

变异测试: 故意在代码中引入错误(变异),验证测试用例能否发现:

  1. 修改排序逻辑为降序
  2. 将a + b <= c改为a + b < c
  3. 删除等边判断中的某个条件

好的测试套件应该能捕获这些故意引入的错误。

6. 测试驱动开发(TDD)实践

让我们尝试用TDD方式重新实现这个功能:

  1. 先写失败测试:

    def test_equilateral():
        assert classify_triangle(1, 1, 1) == "等边三角形"
    
  2. 最小实现:

    def classify_triangle(a, b, c):
        return "等边三角形"
    
  3. 添加新测试:

    def test_isosceles():
        assert classify_triangle(2, 2, 3) == "等腰三角形"
    
  4. 扩展实现:

    def classify_triangle(a, b, c):
        if a == b == c:
            return "等边三角形"
        return "等腰三角形"
    
  5. 逐步完善: 重复这个过程,直到覆盖所有类型和边界条件。

TDD优势:

  • 确保每个功能都有对应测试
  • 更清晰的需求理解
  • 更模块化的代码设计
  • 自然形成高测试覆盖率

7. 性能测试与优化

即使是简单函数,也应考虑性能:

import timeit

def test_performance():
    setup = "from __main__ import classify_triangle"
    stmt = "classify_triangle(1, 1, 1)"
    time = timeit.timeit(stmt, setup, number=100000)
    print(f"10万次执行耗时: {time:.2f}秒")

优化方向:

  • 减少不必要的排序(如果输入已经有序)
  • 使用更快的比较方式
  • 缓存重复计算结果

优化后版本:

def classify_triangle_optimized(a, b, c):
    if not isinstance(a, (int, float)) or not isinstance(b, (int, float)) or not isinstance(c, (int, float)):
        raise ValueError("边长必须为数字")
    if a <= 0 or b <= 0 or c <= 0:
        raise ValueError("边长必须大于0")
    
    # 手动排序避免创建新列表
    if a > b: a, b = b, a
    if b > c: b, c = c, b
    if a > b: a, b = b, a
    
    if a + b <= c:
        return "非三角形"
    
    if a == c:  # 已排序,a <= b <= c,所以a==c意味着a==b==c
        return "等边三角形"
    if a == b or b == c:
        return "等腰三角形"
    return "一般三角形"

性能对比:

  • 原版:10万次1.8秒
  • 优化版:10万次1.2秒

8. 实际项目中的测试策略

在真实项目中,三角形判断可能只是复杂系统的一小部分。完整的测试策略应包括:

  1. 单元测试(本文重点):

    • 测试独立函数
    • 快速反馈
    • 高覆盖率
  2. 集成测试:

    def test_integration():
        result = some_complex_operation_using_triangle()
        assert "等边三角形" in result
    
  3. 端到端测试:

    def test_e2e():
        # 模拟用户从输入到输出的完整流程
        input_values = ["1", "1", "1"]
        with mock.patch('builtins.input', side_effect=input_values):
            output = main_program()
        assert "等边" in output
    
  4. 属性测试: 使用hypothesis库进行随机测试:

    from hypothesis import given
    from hypothesis.strategies import floats, integers
    
    @given(integers(min_value=1))
    def test_equilateral_property(side):
        assert classify_triangle(side, side, side) == "等边三角形"
    

测试金字塔原则:

  • 大量单元测试(快速、独立)
  • 适量集成测试(模块交互)
  • 少量端到端测试(完整流程)
Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐