什么是单元测试?
单元测试(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 -v 或 pytest -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)、独立、无副作用。避免在单元测试中做真实的网络请求或数据库操作。