Python测试用例设计介绍(pytest)AAA模式、FIRST原则、等价类划分、边界值、状态转换测试、参数化测试、Fixture测试夹具、Mock与Patch、pytest-cov插件测试覆盖率

Python测试用例设计介绍(pytest)AAA模式、FIRST原则、等价类划分、边界值、状态转换测试、参数化测试、Fixture测试夹具、Mock与Patch、pytest-cov插件测试覆盖率

文章目录

  • Python 测试用例设计指南:从入门到最佳实践
    • 一、为什么需要写测试?
    • 二、Python 测试工具概览
    • 三、第一个测试用例
      • 3.1 被测函数
      • 3.2 使用 pytest 编写测试
      • 3.3 使用 unittest 编写测试
    • 四、测试用例设计的核心原则
      • 4.1 FIRST 原则
      • 4.2 AAA 模式
      • 4.3 测试命名规范
    • 五、测试用例设计方法
      • 5.1 等价类划分
      • 5.2 边界值分析
      • 5.3 状态转换测试
    • 六、进阶技巧
      • 6.1 参数化测试(Parametrize)
      • 6.2 Fixture(测试夹具)
      • 6.3 Mock 与 Patch
        • 代码解释
          • 1. 核心机制:Mock(模拟/打桩)
          • 2. 预设返回值
          • 3. 验证行为而非结果
          • 为什么要这样做?
          • ⚠️ 注意事项
      • 6.4 测试覆盖率
    • 七、项目测试结构
    • 八、常见反模式(要避免的做法)
    • 九、总结

Python 测试用例设计指南:从入门到最佳实践

“任何未经测试的代码,都是不可靠的代码。”

在软件开发中,测试是保障代码质量的关键环节。Python 作为一门简洁而强大的语言,拥有丰富的测试生态。本文将从零开始,带你掌握 Python 测试用例的设计方法与最佳实践。


一、为什么需要写测试?

很多开发者觉得写测试"浪费时间",但事实上,测试能帮你:

  • 尽早发现 Bug:在代码上线前捕获问题,修复成本远低于线上排错。
  • 重构的信心:有了测试的保护伞,你可以大胆重构而不必担心引入新问题。
  • 活的文档:好的测试用例本身就是代码行为的最佳说明。
  • 团队协作:测试让新成员更快理解模块的预期行为。

二、Python 测试工具概览

工具特点
unittestPython 标准库,开箱即用,xUnit 风格
pytest第三方框架,语法简洁,插件生态丰富,推荐首选
doctest基于文档字符串的测试,适合简单示例
nose2unittest 的扩展,社区活跃度下降

本文以pytest为主进行讲解,同时也会涉及unittest的基本用法。


三、第一个测试用例

3.1 被测函数

# calculator.pydefadd(a:float,b:float)->float:returna+bdefdivide(a:float,b:float)->float:ifb==0:raiseValueError("除数不能为零")returna/b

3.2 使用 pytest 编写测试

# test_calculator.pyimportpytestfromcalculatorimportadd,dividedeftest_add_positive_numbers():assertadd(2,3)==5deftest_add_negative_numbers():assertadd(-1,-1)==-2deftest_add_mixed_numbers():assertadd(-1,1)==0deftest_divide_normal():assertdivide(10,2)==5.0deftest_divide_by_zero():withpytest.raises(ValueError,match="除数不能为零"):divide(10,0)

运行测试:

$ pytest test_calculator.py-v

3.3 使用 unittest 编写测试

# test_calculator_unittest.pyimportunittestfromcalculatorimportadd,divideclassTestCalculator(unittest.TestCase):deftest_add_positive_numbers(self):self.assertEqual(add(2,3),5)deftest_divide_by_zero(self):withself.assertRaises(ValueError):divide(10,0)if__name__=="__main__":unittest.main()

四、测试用例设计的核心原则

4.1 FIRST 原则

原则说明
Fast(快速)测试必须运行得快,否则你会越来越不愿意运行它
Isolated(隔离)每个测试应独立运行,不依赖其他测试的执行顺序或结果
Repeatable(可重复)在任何环境下运行,结果都应一致
Self-Validating(自验证)测试结果应由断言自动判断,不需要人工检查
Timely(及时)测试应在编写代码的同时(或之前)编写

4.2 AAA 模式

每个测试用例应遵循Arrange-Act-Assert结构:

deftest_shopping_cart_total():# Arrange(准备)cart=ShoppingCart()cart.add_item("Python书",price=59.0)cart.add_item("键盘",price=199.0)# Act(执行)total=cart.get_total()# Assert(断言)asserttotal==258.0

4.3 测试命名规范

好的测试名称应该清晰表达测试意图

# ❌ 不好的命名deftest_1():...deftest_divide():...# ✅ 好的命名deftest_divide_returns_correct_quotient():...deftest_divide_raises_error_when_divisor_is_zero():...

五、测试用例设计方法

5.1 等价类划分

将输入数据划分为若干"等价类",每个类中取一个代表值进行测试:

# 判断年龄阶段defclassify_age(age:int)->str:ifage<0:raiseValueError("年龄不能为负数")elifage<18:return"未成年"elifage<60:return"成年人"else:return"老年人"

等价类划分:

等价类代表值预期结果
负数(无效)-1ValueError
0~17(未成年)10“未成年”
18~59(成年人)30“成年人”
60+(老年人)70“老年人”
边界值 00“未成年”
边界值 1818“成年人”
边界值 6060“老年人”

5.2 边界值分析

Bug 往往隐藏在边界条件中,重点测试边界值:

@pytest.mark.parametrize("age, expected",[(-1,"error"),# 无效下界(0,"未成年"),# 有效下界(17,"未成年"),# 区间上界(18,"成年人"),# 新区间下界(59,"成年人"),# 区间上界(60,"老年人"),# 新区间下界(150,"老年人"),# 极端值])deftest_classify_age(age,expected):ifexpected=="error":withpytest.raises(ValueError):classify_age(age)else:assertclassify_age(age)==expected

5.3 状态转换测试

对于有状态的对象,测试不同状态之间的转换:

classOrder:def__init__(self):self.status="created"defpay(self):ifself.status!="created":raiseRuntimeError("只有已创建的订单可以支付")self.status="paid"defship(self):ifself.status!="paid":raiseRuntimeError("只有已支付的订单可以发货")self.status="shipped"defcancel(self):ifself.status=="shipped":raiseRuntimeError("已发货的订单不能取消")self.status="cancelled"
deftest_order_lifecycle_happy_path():"""正常流程:创建 → 支付 → 发货"""order=Order()order.pay()assertorder.status=="paid"order.ship()assertorder.status=="shipped"deftest_order_cannot_ship_before_payment():"""未支付不能发货"""order=Order()withpytest.raises(RuntimeError):order.ship()deftest_order_cannot_cancel_after_shipping():"""已发货不能取消"""order=Order()order.pay()order.ship()withpytest.raises(RuntimeError):order.cancel()

六、进阶技巧

6.1 参数化测试(Parametrize)

避免重复代码,用一组数据驱动多个测试:

@pytest.mark.parametrize("input_str, expected",[("hello","HELLO"),("World","WORLD"),("123abc","123ABC"),("",""),])deftest_to_uppercase(input_str,expected):assertinput_str.upper()==expected

6.2 Fixture(测试夹具)

用 Fixture 管理测试的前置条件和资源清理:

importpytestimporttempfileimportos@pytest.fixturedeftemp_file():"""创建一个临时文件,测试结束后自动清理"""fd,path=tempfile.mkstemp()withopen(path,"w")asf:f.write("hello world")yieldpath# 将路径传递给测试函数# 清理os.close(fd)os.unlink(path)deftest_read_file(temp_file):withopen(temp_file)asf:content=f.read()assertcontent=="hello world"

6.3 Mock 与 Patch

当测试涉及外部依赖(数据库、API、文件系统等)时,使用 Mock 隔离:

fromunittest.mockimportpatch,MagicMockfrommy_serviceimportUserServicedeftest_get_user_from_api():withpatch("my_service.requests.get")asmock_get:# 模拟 API 返回mock_response=MagicMock()mock_response.json.return_value={"name":"Alice","age":30}mock_response.status_code=200mock_get.return_value=mock_response# 执行测试service=UserService()user=service.get_user(1)# 断言assertuser["name"]=="Alice"mock_get.assert_called_once_with("https://api.example.com/users/1")
代码解释

这段代码没有发送真正的网络请求。

这是 Python 单元测试中unittest.mock库的核心用法,具体解释如下:

1. 核心机制:Mock(模拟/打桩)
withpatch("my_service.requests.get")asmock_get:

这行代码做了两件事:

  • 拦截:在with块的作用域内,将my_service模块中引用的requests.get临时替换为一个MagicMock对象。
  • 隔离:当UserService.get_user()内部调用requests.get(...)时,它调用的其实是这个 Mock 对象,而不是真正的 HTTP 客户端。with块结束后,原始的requests.get会自动恢复。
2. 预设返回值
mock_response.json.return_value={"name":"Alice","age":30}mock_response.status_code=200mock_get.return_value=mock_response

这里手动构造了一个"假响应",告诉 Mock 对象:

“当别人调用你时,返回这个我预先设定好的 response 对象”

所以service.get_user(1)拿到的数据完全是你编造的,不依赖任何外部服务。

3. 验证行为而非结果
mock_get.assert_called_once_with("https://api.example.com/users/1")

这行断言验证的是:你的业务代码是否以正确的参数调用了 API。即使没有真实请求,也能确认 URL 拼接逻辑是否正确。

为什么要这样做?
真实请求的问题Mock 的优势
依赖外部服务可用性✅ 完全离线运行
网络延迟导致测试慢✅ 毫秒级执行
API 返回数据可能变化✅ 结果确定、可重复
难以覆盖异常/边界场景✅ 可轻松模拟 404、超时等
可能产生真实副作用✅ 零副作用
⚠️ 注意事项
  • Patch 路径必须是"被测模块中的引用路径",即"my_service.requests.get"而非"requests.get"。如果写错路径,Mock 不会生效,可能会意外发出真实请求。
  • Mock 只验证了集成点的调用方式,不能替代对真实 API 的集成测试/契约测试。建议单元测试用 Mock,另设少量集成测试验证真实连通性。

总结:这段代码是一个标准的纯单元测试,通过 Mock 将外部依赖替换为可控的假对象,从而快速、可靠地验证UserService自身的业务逻辑。

6.4 测试覆盖率

使用pytest-cov插件检查测试覆盖率:

$ pytest--cov=my_module --cov-report=term-missing
---------- coverage: platform linux, python 3.11 ---------- Name Stmts Miss Cover Missing ---------------------------------------------------- calculator.py 8 1 88% 7 test_calculator.py 12 0 100% ---------------------------------------------------- TOTAL 20 1 95%

💡建议:不要盲目追求 100% 覆盖率,而应关注关键路径边界条件是否被充分测试。一般来说,80% 以上的覆盖率是一个合理的目标。


七、项目测试结构

推荐的项目目录结构:

my_project/ ├── src/ │ └── my_module/ │ ├── __init__.py │ ├── core.py │ └── utils.py ├── tests/ │ ├── conftest.py # 共享的 Fixture │ ├── test_core.py │ └── test_utils.py ├── pyproject.toml # 配置 pytest └── README.md

pyproject.toml中配置 pytest:

[tool.pytest.ini_options] testpaths = ["tests"] addopts = "-v --tb=short"

八、常见反模式(要避免的做法)

❌ 反模式说明✅ 正确做法
测试之间共享状态导致测试相互依赖,顺序敏感每个测试独立准备数据
测试中包含硬编码路径在不同环境无法运行使用 Fixture 或临时文件
过度使用 Mock测试脱离了真实行为只在必要时 Mock 外部依赖
一个测试测太多东西失败时难以定位问题每个测试只验证一个行为
只测 Happy Path忽略了异常和边界情况同时覆盖正常和异常路径

九、总结

设计优秀的 Python 测试用例,核心在于:

  1. 选对工具:推荐 pytest,简洁高效。
  2. 遵循原则:FIRST 原则 + AAA 模式。
  3. 运用方法:等价类划分、边界值分析、状态转换。
  4. 善用技巧:参数化、Fixture、Mock、覆盖率分析。
  5. 避开陷阱:保持测试独立、简洁、可维护。

记住,测试不是负担,而是你对代码质量的投资。今天多花 10 分钟写测试,明天就少花 2 小时排查 Bug。

Happy Testing! 🧪✨