单元测试

Python 单元测试最佳实践指南

什么是单元测试?

单元测试是用来对一个模块、一个函数或者一个类进行正确性检验的测试工作。测试驱动开发(TDD)是一种先写测试再实现功能的开发模式,能够确保代码质量。

为什么需要单元测试?

  1. 验证功能正确性:确保代码按预期工作
  2. 防止回归错误:修改代码后快速验证原有功能
  3. 提高代码质量:促使开发者编写可测试的模块化代码
  4. 文档作用:测试用例本身就是如何使用代码的示例

现代单元测试实践

1. 使用 pytest 替代 unittest

虽然 Python 自带的 unittest 模块仍然可用,但 pytest 已成为更流行的选择,因为它:

  • 语法更简洁
  • 提供更丰富的断言
  • 有更强大的 fixture 系统
  • 支持参数化测试
  • 有丰富的插件生态
# 使用 pytest 重写测试示例
import pytest
from mydict import Dict

def test_dict_init():
    d = Dict(a=1, b='test')
    assert d.a == 1
    assert d.b == 'test'
    assert isinstance(d, dict)

2. 测试用例设计原则

好的测试应该遵循 FIRST 原则:

  • Fast (快速)
  • Isolated (隔离)
  • Repeatable (可重复)
  • Self-validating (自验证)
  • Timely (及时)

3. 测试覆盖率

使用 pytest-cov 插件检查测试覆盖率:

pytest --cov=mymodule tests/

理想情况下应达到 80% 以上的覆盖率,关键业务代码应达到 100%。

现代测试示例

1. 测试字典类

# mydict.py
class Dict(dict):
    """支持属性访问的字典类"""
    def __init__(self, **kw):
        super().__init__(**kw)

    def __getattr__(self, key):
        try:
            return self[key]
        except KeyError:
            raise AttributeError(f"'Dict' object has no attribute '{key}'")

    def __setattr__(self, key, value):
        self[key] = value
# test_mydict.py
import pytest
from mydict import Dict

class TestDict:
    def test_init(self):
        d = Dict(a=1, b='test')
        assert d.a == 1
        assert d.b == 'test'
        assert isinstance(d, dict)

    def test_key_access(self):
        d = Dict()
        d['key'] = 'value'
        assert d.key == 'value'

    def test_attr_access(self):
        d = Dict()
        d.key = 'value'
        assert 'key' in d
        assert d['key'] == 'value'

    def test_key_error(self):
        d = Dict()
        with pytest.raises(KeyError):
            _ = d['empty']

    def test_attr_error(self):
        d = Dict()
        with pytest.raises(AttributeError):
            _ = d.empty

2. 使用 fixture

@pytest.fixture
def sample_dict():
    return Dict(a=1, b=2)

def test_fixture_usage(sample_dict):
    assert sample_dict.a == 1
    assert sample_dict['b'] == 2

3. 参数化测试

@pytest.mark.parametrize("input,expected", [
    (80, 'A'),
    (100, 'A'),
    (60, 'B'),
    (79, 'B'),
    (0, 'C'),
    (59, 'C'),
])
def test_grade_boundaries(input, expected):
    s = Student('Test', input)
    assert s.get_grade() == expected

修正 Student 类

原 Student 类中的 get_grade() 方法有逻辑错误,边界条件处理不当:

class Student:
    def __init__(self, name, score):
        self.name = name
        if not 0 <= score <= 100:
            raise ValueError("Score must be between 0 and 100")
        self.score = score
        
    def get_grade(self):
        if self.score >= 80:
            return 'A'
        if self.score >= 60:
            return 'B'
        return 'C'

测试执行

使用 pytest 运行测试:

# 运行所有测试
pytest

# 运行特定测试文件
pytest test_mydict.py

# 运行特定测试类
pytest test_mydict.py::TestDict

# 运行单个测试方法
pytest test_mydict.py::TestDict::test_init

# 显示详细输出
pytest -v

# 按名称过滤测试
pytest -k "test_init or test_key"

最佳实践建议

  1. 测试命名:测试名称应该清晰描述测试内容
  2. 独立测试:每个测试应该独立运行,不依赖其他测试状态
  3. 测试异常:使用 pytest.raises 测试预期异常
  4. 避免测试实现细节:测试行为而非实现
  5. 定期运行测试:集成到 CI/CD 流程中
  6. Mock 外部依赖:使用 pytest-mockunittest.mock

总结

现代 Python 单元测试已经从简单的 unittest 发展为更强大的 pytest 生态系统。良好的测试实践可以显著提高代码质量和开发效率。记住:

  • 测试应该简单、快速、独立
  • 覆盖正常路径、边界条件和异常情况
  • 测试通过是基本要求,但不是质量保证的全部
  • 将测试作为开发流程的核心部分

通过遵循这些实践,您可以构建更健壮、更易维护的 Python 应用程序。