单元测试

pytest / unittest / doctest 入门,编写高质量的自动化测试用例

什么是单元测试?

单元测试(Unit Testing)是软件开发中针对最小可测试单元(通常是函数或方法)进行的测试,确保每个代码单元在隔离环境下都能按预期工作。单元测试是自动化测试金字塔的基础,具有快速、稳定、成本低的特点。

为什么要单元测试?
尽早发现 Bug、提高代码质量、便于重构、作为活文档。覆盖率应达到 80% 以上。
单元测试原则(FIRST)
Fast(快速)、Independent(独立)、Repeatable(可重复)、Self-validating(自验证)、Timely(及时)。
常用框架
pytest(推荐)、unittest(标准库)、doctest(文档测试)、nose。pytest 最为灵活强大。
测试覆盖率
行覆盖、分支覆盖、条件覆盖。推荐使用 coverage.py 或 pytest-cov 测量。
Mock 技术
使用 unittest.mock 模拟外部依赖(数据库、网络等),保持测试快速稳定。
常见陷阱
测试之间有依赖、测试真实环境、断言模糊、测试逻辑太复杂。

pytest 基础入门

安装 pytest

# 安装 pytest pip install pytest # 安装常用插件 pip install pytest-cov pytest-html pytest-xdist pytest-mock

第一个 pytest 测试

# test_basic.py def add(a, b): return a + b def subtract(a, b): return a - b class TestCalculator: def test_add(self): assert add(2, 3) == 5 assert add(-1, 1) == 0 assert add(0, 0) == 0 def test_subtract(self): assert subtract(5, 3) == 2 assert subtract(1, 1) == 0 def test_add_error(self): with pytest.raises(TypeError): add("a", 1)
运行命令:pytest test_basic.py -vpytest -k "test_add" -v

pytest Fixture 机制

Fixture 是 pytest 最强大的功能,用于管理测试前置条件和清理操作。

# conftest.py - Fixture 配置 import pytest import tempfile import os @pytest.fixture def temp_dir(): # 前置:创建临时目录 tmpdir = tempfile.mkdtemp() yield tmpdir # 后置:清理临时目录 import shutil shutil.rmtree(tmpdir) @pytest.fixture(scope="module") def test_user(): return {"name": "admin", "pwd": "123456"}
# test_with_fixture.py - 使用 Fixture def test_user_login(test_user): assert test_user["name"] == "admin" assert len(test_user["pwd"]) >= 6 def test_temp_file(temp_dir): filepath = os.path.join(temp_dir, "test.txt") with open(filepath, "w") as f: f.write("hello") assert os.path.exists(filepath)

scope 参数

控制 Fixture 生命周期:function / class / module / session

yield 关键字

yield 前执行前置,yield 后执行清理(teardown)

自动注入

只需在测试函数参数中声明,pytest 自动匹配 Fixture

Fixture 嵌套

Fixture 可以依赖其他 Fixture,灵活组合

Mock 与 Patch

使用 unittest.mock 模拟外部依赖,使单元测试不依赖真实环境。

from unittest.mock import Mock, patch class TestUserService: @patch('app.db.Database.query') def test_get_user(self, mock_query): # 模拟数据库返回 mock_query.return_value = {"id": 1, "name": "Alice"} result = get_user(1) assert result["name"] == "Alice" mock_query.assert_called_once_with(1) @patch('requests.get') def test_api_call(self, mock_get): mock_response = Mock() mock_response.status_code = 200 mock_response.json.return_value = {"status": "ok"} mock_get.return_value = mock_response assert fetch_data() == {"status": "ok"}

pytest 高级用法

参数化测试

import pytest @pytest.mark.parametrize("input_val,expected", [ (2, 4), (3, 9), (4, 16), (0, 0), (-2, 4), ]) def test_square(input_val, expected): assert input_val ** 2 == expected

标记与分组

# pytest.ini 中注册自定义标记 # [tool.pytest.ini_options] # markers = ["slow: 慢速测试", "smoke: 冒烟测试", "regression: 回归测试"] @pytest.mark.slow def test_large_data(): pass @pytest.mark.smoke def test_quick_check(): pass # 运行指定标记:pytest -m "smoke" # 排除标记:pytest -m "not slow"

生成测试报告

# 生成 HTML 报告 pytest --html=report.html --self-contained-html # 生成 JUnit XML 报告(CI 用) pytest --junitxml=result.xml # 覆盖率报告 pytest --cov=src --cov-report=html
注意:单元测试应保持快速(< 100ms)、独立、无副作用。避免在单元测试中做真实的网络请求或数据库操作。