第 25 章 测试
25.1 unittest 模块
Python 内置的单元测试框架,风格类似 Java 的 JUnit:
import unittest
def add(a, b):
return a + b
def divide(a, b):
if b == 0:
raise ValueError("除数不能为零")
return a / b
class TestMath(unittest.TestCase):
def setUp(self):
"""每个测试方法执行前调用"""
self.data = [1, 2, 3, 4, 5]
def tearDown(self):
"""每个测试方法执行后调用"""
pass
def test_add(self):
self.assertEqual(add(1, 2), 3)
self.assertEqual(add(-1, 1), 0)
self.assertEqual(add(0, 0), 0)
def test_add_float(self):
self.assertAlmostEqual(add(0.1, 0.2), 0.3, places=7)
def test_divide(self):
self.assertEqual(divide(10, 2), 5)
self.assertAlmostEqual(divide(1, 3), 0.333, places=3)
def test_divide_by_zero(self):
with self.assertRaises(ValueError):
divide(1, 0)
def test_types(self):
self.assertIsInstance(add(1, 2), int)
self.assertTrue(add(1, 2) > 0)
self.assertFalse(add(1, -1) > 0)
self.assertIsNone(None)
self.assertIn(3, self.data)
if __name__ == "__main__":
unittest.main()
常用断言方法
| 方法 | 检查 |
|---|---|
assertEqual(a, b) | a == b |
assertNotEqual(a, b) | a != b |
assertTrue(x) | bool(x) is True |
assertFalse(x) | bool(x) is False |
assertIs(a, b) | a is b |
assertIsNone(x) | x is None |
assertIn(a, b) | a in b |
assertIsInstance(a, b) | isinstance(a, b) |
assertRaises(exc) | 抛出指定异常 |
assertAlmostEqual(a, b) | 近似相等 |
python -m unittest test_math.py
python -m unittest test_math.TestMath.test_add # 运行单个测试
python -m unittest discover -s tests # 发现并运行所有测试
25.2 doctest — 文档即测试
把测试写在文档字符串中,同时充当示例文档:
def factorial(n):
"""计算阶乘。
>>> factorial(0)
1
>>> factorial(1)
1
>>> factorial(5)
120
>>> factorial(-1)
Traceback (most recent call last):
...
ValueError: n must be >= 0
"""
if n < 0:
raise ValueError("n must be >= 0")
if n <= 1:
return 1
return n * factorial(n - 1)
if __name__ == "__main__":
import doctest
doctest.testmod(verbose=True)
python -m doctest your_module.py -v
适用场景:简单函数的示例测试,不适合复杂测试逻辑。
25.3 pytest 测试实践
pytest 是 Python 社区最流行的测试框架,语法更简洁:
pip install pytest
基本用法
# test_example.py
def add(a, b):
return a + b
# 测试函数以 test_ 开头
def test_add():
assert add(1, 2) == 3
assert add(-1, 1) == 0
def test_add_float():
assert add(0.1, 0.2) == pytest.approx(0.3)
# 测试异常
import pytest
def test_divide_by_zero():
with pytest.raises(ValueError, match="除数不能为零"):
divide(1, 0)
pytest # 运行所有测试
pytest test_example.py # 运行指定文件
pytest test_example.py::test_add # 运行指定测试
pytest -v # 详细输出
pytest -x # 第一个失败就停止
pytest -k "add" # 只运行名字含 "add" 的测试
fixture — 测试夹具
import pytest
@pytest.fixture
def sample_data():
"""提供测试数据"""
return [1, 2, 3, 4, 5]
@pytest.fixture
def db_connection():
"""管理资源的生命周期"""
conn = create_connection()
yield conn # yield 之前是 setup,之后是 teardown
conn.close()
def test_sum(sample_data):
assert sum(sample_data) == 15
def test_length(sample_data):
assert len(sample_data) == 5
# fixture 作用域
@pytest.fixture(scope="session") # 整个测试会话只执行一次
def app_config():
return load_config()
@pytest.fixture(scope="module") # 每个模块执行一次
def module_data():
return prepare_data()
参数化测试
import pytest
@pytest.mark.parametrize("a, b, expected", [
(1, 2, 3),
(-1, 1, 0),
(0, 0, 0),
(100, 200, 300),
])
def test_add(a, b, expected):
assert add(a, b) == expected
@pytest.mark.parametrize("input_val, expected", [
("hello", 5),
("", 0),
("Python", 6),
])
def test_string_length(input_val, expected):
assert len(input_val) == expected
标记
import pytest
@pytest.mark.slow
def test_slow_operation():
"""需要较长时间的测试"""
pass
@pytest.mark.skip(reason="功能尚未实现")
def test_future_feature():
pass
@pytest.mark.skipif(sys.platform == "win32", reason="仅限 Linux")
def test_linux_only():
pass
@pytest.mark.xfail(reason="已知 bug")
def test_known_bug():
assert broken_function() == expected
pytest -m slow # 只运行标记为 slow 的测试
pytest -m "not slow" # 跳过 slow 测试
25.4 mock 与 patch
模拟外部依赖,隔离测试:
from unittest.mock import Mock, patch, MagicMock
# Mock 基本用法
mock_api = Mock()
mock_api.get_user.return_value = {"name": "Alice", "age": 30}
result = mock_api.get_user(1)
print(result) # {'name': 'Alice', 'age': 30}
mock_api.get_user.assert_called_once_with(1)
# Mock 设置副作用
mock_api.get_user.side_effect = ConnectionError("网络错误")
# mock_api.get_user(1) # 抛出 ConnectionError
# patch — 替换模块中的对象
# user_service.py
import requests
def get_user(user_id):
response = requests.get(f"https://api.com/users/{user_id}")
return response.json()
# test_user_service.py
from unittest.mock import patch
@patch("user_service.requests.get")
def test_get_user(mock_get):
mock_get.return_value.json.return_value = {"name": "Alice"}
mock_get.return_value.status_code = 200
result = get_user(1)
assert result == {"name": "Alice"}
mock_get.assert_called_once_with("https://api.com/users/1")
# 用作上下文管理器
def test_with_context():
with patch("user_service.requests.get") as mock_get:
mock_get.return_value.json.return_value = {"name": "Bob"}
result = get_user(2)
assert result["name"] == "Bob"
pytest 的 monkeypatch
def test_env_variable(monkeypatch):
monkeypatch.setenv("API_KEY", "test_key_123")
import os
assert os.environ["API_KEY"] == "test_key_123"
def test_replace_function(monkeypatch):
monkeypatch.setattr("time.time", lambda: 1000.0)
import time
assert time.time() == 1000.0
25.5 测试覆盖率与代码质量
pip install pytest-cov
# 运行测试并生成覆盖率报告
pytest --cov=mypackage --cov-report=html
pytest --cov=mypackage --cov-report=term-missing
# 输出示例:
# Name Stmts Miss Cover Missing
# --------------------------------------------------
# mypackage/core.py 50 5 90% 23-25, 42, 67
# mypackage/utils.py 30 0 100%
# --------------------------------------------------
# TOTAL 80 5 94%
pytest 配置
# pyproject.toml
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = "-v --cov=src --cov-report=term-missing"
markers = [
"slow: marks tests as slow",
"integration: marks integration tests",
]
本章小结:测试是专业开发的基石。unittest 是内置方案,pytest 是社区标准(更简洁、更强大)。核心技能:用 fixture 管理测试资源、用参数化减少重复、用 mock/patch 隔离外部依赖、用覆盖率报告发现测试盲区。目标是让测试成为开发习惯而非负担。