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+, 且pytest
4.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 的测试函数执行完毕后(无论成功或失败)执行。