首页 / 知识库 / 0基础入门-阅读资料 / 0基础-python入门到精通

第 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 隔离外部依赖、用覆盖率报告发现测试盲区。目标是让测试成为开发习惯而非负担。