Python(十九):第十八章 单元测试
Python(十九):第十八章 单元测试
Prorise第十八章 单元测试
软件开发的核心目标之一是交付高质量、运行稳定的代码。单元测试 (Unit Testing) 是保障这一目标的重要手段,它专注于验证软件中最小可测试单元(通常是函数、方法或类)的行为是否符合预期。
在本章中,我们将深入学习 Python 中广受欢迎的测试框架 —— pytest。
18.1 单元测试简介:
单元测试通过在开发早期发现并修复问题,从而提升代码质量,增强代码重构的信心,并作为一种“活文档”辅助理解代码功能。
基本流程:
- 隔离单元:确定要测试的函数、方法或类。
- 定义预期:明确该单元在特定输入下应有的输出或行为。
- 编写测试:使用测试框架编写代码来验证这些预期。
- 执行测试:运行测试并检查结果。
- 迭代优化:根据测试结果修改代码或测试本身。
18.2 Pytest 简介与核心优势
pytest 是一个成熟且功能齐全的 Python 测试框架,它使得编写小型、易读的测试变得简单,并且可以扩展以支持复杂的函数式、接口或系统级测试。
为什么选择 pytest?
- 极简样板代码:相比
unittest,pytest需要的模板代码更少。测试函数就是普通的 Python 函数,不需要继承任何类。 - 强大的
assert语句:直接使用标准的assert语句进行断言,pytest会提供详细的断言失败信息。 - 灵活的 Fixtures:
pytest的 Fixture 系统非常强大,用于管理测试依赖和测试上下文的准备与清理,比传统的setUp/tearDown更灵活。 - 丰富的插件生态:拥有大量高质量的第三方插件(如
pytest-django,pytest-cov(覆盖率),pytest-xdist(并行测试) 等)。 - 良好的兼容性:可以运行基于
unittest和nose编写的测试用例。 - 清晰的测试报告:默认提供易读的测试报告。
安装 pytest
您可以使用 pip 来安装 pytest:
1 | pip install pytest |
18.3 Pytest 核心特性概览
pytest 的强大功能主要体现在以下几个核心特性上,本笔记将逐一介绍:
| 特性/概念 | 简介 | 涉及的主要 pytest 元素/用法 |
|---|---|---|
| 测试发现 (Test Discovery) | pytest 自动查找符合特定命名约定的测试文件和函数。 | 文件名 test_*.py 或 *_test.py;函数/方法名 test_*。 |
| 基本测试函数 (Basic Test Functions) | 普通 Python 函数即可作为测试用例,无需继承特定类。 | def test_example(): ... |
| 断言 (Assertions) | 使用 Python 内置的 assert 语句进行结果验证,pytest 提供详细的错误报告。 | assert expression |
| 异常测试 (Exception Testing) | 优雅地测试代码是否按预期抛出异常。 | pytest.raises() 上下文管理器。 |
| Fixtures (测试固件) | 管理测试函数的依赖、状态和资源,实现代码复用和模块化。 | @pytest.fixture 装饰器, yield 用于 teardown。 |
| 参数化测试 (Parametrization) | 使用不同的参数多次运行同一个测试函数,避免代码重复。 | @pytest.mark.parametrize 装饰器。 |
| 标记 (Markers) | 为测试函数添加元数据,用于分类、跳过、标记预期失败等。 | @pytest.mark.<marker_name> (如 skip, xfail)。 |
| 运行测试 (Running Tests) | 通过命令行工具 pytest 运行测试,并提供多种选项。 | pytest 命令及其参数 (如 -v, -k, -m)。 |
18.4 Pytest 基础实践
18.4.1 编写第一个 Pytest 测试:测试加法函数
pytest 会自动发现当前目录及其子目录下所有命名为 test_*.py 或 *_test.py 的文件中的 test_* 开头的函数。
被测代码 (my_math.py):
1 | # my_math.py |
测试代码 (test_my_math_pytest.py):
1 | # test_my_math_pytest.py |
代码注释与讲解:
- 测试文件命名为
test_my_math_pytest.py,遵循pytest的发现约定。 - 测试函数
test_simple_addition和test_negative_addition以test_开头。 - 直接使用
assert语句。如果assert后的表达式为False,pytest会将该测试标记为失败,并提供详细的上下文信息。 - 最后的
, "message"部分是可选的,如果断言失败,这个消息不会像unittest的msg参数那样直接显示,pytest会通过其内省机制提供更丰富的失败信息。
运行测试:

pytest 会自动找到并执行 test_simple_addition 和 test_negative_addition。
18.4.2 测试异常:pytest.raises
当需要验证代码是否按预期抛出特定异常时,可以使用 pytest.raises。
被测代码 (more_math.py,包含 square 函数):
1 | # more_math.py |
测试代码 (test_more_math_pytest.py):
1 | # test_more_math_pytest.py |
18.5 Pytest Fixtures:强大的依赖注入与测试准备
Fixtures(测试固件)是 pytest 中一个非常核心且强大的特性。它们用于为测试函数、类、模块或整个会话设置必要的预置条件(如数据、对象实例、服务连接等),并在测试结束后进行清理。
18.5.1 Fixture 基本概念与应用:@pytest.fixture
通过 @pytest.fixture 装饰器可以将一个函数标记为 fixture。测试函数可以通过将其名称作为参数来请求使用这个 fixture。
示例 (test_fixtures_basic.py):
1 | # test_fixtures_basic.py |
代码注释与讲解:
@pytest.fixture: 装饰器,将sample_list和sample_dict函数转换为 fixture。- 当测试函数(如
test_list_length(sample_list))在其参数列表中包含 fixture 名称时,pytest会在执行该测试函数之前先执行对应的 fixture 函数,并将其返回值注入到测试函数的同名参数中。 - 复用性:同一个 fixture 可以被多个测试函数使用,避免了重复的设置代码。
- 声明式依赖:测试函数清晰地声明了它所依赖的上下文或数据。
18.5.2 Fixture 的作用域 (Scope)
Fixture 可以定义不同的作用域,以控制其执行(setup/teardown)的频率和生命周期。作用域通过 @pytest.fixture 的 scope 参数指定
作为单元测试来说,没有必要区分的这么死板,平常来说使用默认值即可,若有严格需求再详细区分作用域
function(默认): 每个测试函数执行一次。是开销最小、隔离性最好的作用域。class: 每个测试类执行一次。用于类中所有测试方法共享的、创建开销较大的资源。module: 每个模块执行一次。package: 每个包执行一次 (Python 3.7+, 且pytest4.3+)。session: 整个测试会话(即一次pytest命令的完整执行过程)执行一次。适用于全局的、创建非常昂贵的资源。
示例 (test_fixture_scopes.py):
1 | # test_fixture_scopes.py |
代码注释与讲解:
- 观察运行
pytest -s -v test_fixture_scopes.py(-s用于显示 print 输出) 时的输出,可以清晰地看到不同作用域 fixture 的 setup 时机。
18.5.3 使用 yield 实现 Fixture 的 Teardown (清理操作)
如果 fixture 需要在测试使用完毕后执行清理操作(类似于 unittest 中的 tearDown),可以使用 yield 语句。yield 之前的代码是 setup 部分,yield 之后的代码是 teardown 部分。
示例 (使用 pytest fixture):
1 | # test_file_fixture_pytest.py |
代码注释与讲解:
yield file_path, file_content:yield语句是 setup 和 teardown 的分界点。它将file_path和file_content提供给测试函数。yield之后的代码 (os.remove(file_path)) 在使用该 fixture 的测试函数执行完毕后(无论成功或失败)执行。








