跳转至

03 - 单元测试

学习时间: 2小时 重要性: ⭐⭐⭐⭐ 保证代码质量


🎯 学习目标

  • 理解测试的重要性
  • 掌握pytest的基本使用
  • 学会编写测试用例
  • 了解测试覆盖率

✅ 为什么需要测试?

没有测试的问题

Python
# 你修改了一个函数
def calculate_total(price, quantity):
    return price * quantity  # 之前是 price + quantity

# 不确定是否破坏了其他地方的调用
# 不敢上线,担心出问题

有测试的好处

Python
# 修改代码后运行测试
# pytest
# ============================= test session starts ==============================
# collected 10 items
#
# test_calculator.py ..........                                           [100%]
#
# ============================== 10 passed in 0.5s ===============================

# 所有测试通过,自信上线!

测试的价值: - ✅ 确保代码按预期工作 - ✅ 修改代码时有安全感 - ✅ 文档化代码行为 - ✅ 帮助设计更好的代码


🧪 pytest基础

安装

Bash
pip install pytest
pip install pytest-cov  # 覆盖率插件

第一个测试

Python
# test_calculator.py

def add(a, b):
    return a + b

def test_add():
    """测试加法函数"""
    assert add(1, 2) == 3
    assert add(-1, 1) == 0
    assert add(0, 0) == 0

运行测试:

Bash
pytest test_calculator.py -v

📝 编写测试

AAA模式:Arrange, Act, Assert

Python
def test_calculate_average():
    """测试计算平均值"""
    # Arrange - 准备测试数据
    numbers = [1, 2, 3, 4, 5]

    # Act - 执行被测试的函数
    result = calculate_average(numbers)

    # Assert - 验证结果
    assert result == 3.0

常用断言

Python
def test_assertions():
    # 相等
    assert 1 + 1 == 2

    # 不相等
    assert 1 + 1 != 3

    # 真值判断
    assert True
    assert not False

    # 包含
    assert 3 in [1, 2, 3]
    assert "hello" in "hello world"

    # 异常
    with pytest.raises(ZeroDivisionError):  # 上下文管理器:断言此代码块内会抛出ZeroDivisionError,否则测试失败
        1 / 0

    # 近似相等(浮点数比较)
    assert 0.1 + 0.2 == pytest.approx(0.3)  # approx处理浮点精度问题,0.1+0.2实际是0.30000000000000004

🔧 测试函数和类

测试简单函数

Python
# utils.py
def is_even(n):
    return n % 2 == 0

def divide(a, b):
    if b == 0:
        raise ValueError("除数不能为0")
    return a / b

# test_utils.py
def test_is_even():
    assert is_even(2) is True
    assert is_even(3) is False
    assert is_even(0) is True
    assert is_even(-2) is True

def test_divide():
    assert divide(10, 2) == 5
    assert divide(7, 2) == 3.5

    # 测试异常
    with pytest.raises(ValueError, match="除数不能为0"):
        divide(10, 0)

测试类

Python
# calculator.py
class Calculator:
    def __init__(self):
        self.history = []

    def add(self, a, b):
        result = a + b
        self.history.append(f"{a} + {b} = {result}")
        return result

    def clear_history(self):
        self.history.clear()

# test_calculator.py
class TestCalculator:
    def setup_method(self):
        """每个测试方法前执行"""
        self.calc = Calculator()

    def test_add(self):
        assert self.calc.add(1, 2) == 3
        assert self.calc.add(-1, 1) == 0

    def test_history(self):
        self.calc.add(1, 2)
        self.calc.add(3, 4)
        assert len(self.calc.history) == 2

    def test_clear_history(self):
        self.calc.add(1, 2)
        self.calc.clear_history()
        assert len(self.calc.history) == 0

🎯 测试夹具(Fixtures)

什么是Fixture?

Fixture用于准备测试环境,如创建临时文件、数据库连接等。

Python
import pytest
import tempfile
import os

@pytest.fixture  # @pytest.fixture定义测试夹具,提供可复用的测试资源
def temp_file():
    """创建临时文件"""
    fd, path = tempfile.mkstemp()
    yield path  # 返回给测试使用
    os.close(fd)
    os.unlink(path)  # 测试结束后清理

def test_file_operations(temp_file):
    """使用临时文件进行测试"""
    with open(temp_file, 'w') as f:
        f.write("test data")

    with open(temp_file, 'r') as f:
        content = f.read()

    assert content == "test data"

常用内置Fixtures

Python
def test_with_tmp_path(tmp_path):
    """使用临时目录"""
    file_path = tmp_path / "test.txt"
    file_path.write_text("hello")
    assert file_path.read_text() == "hello"

def test_with_monkeypatch(monkeypatch):
    """修改环境变量"""
    monkeypatch.setenv("API_KEY", "test_key")
    assert os.getenv("API_KEY") == "test_key"

📊 参数化测试

使用pytest.mark.parametrize

Python
import pytest

@pytest.mark.parametrize("input,expected", [
    (1, 1),
    (2, 4),
    (3, 9),
    (4, 16),
    (5, 25),
])
def test_square(input, expected):
    assert input ** 2 == expected

@pytest.mark.parametrize("a,b,expected", [
    (1, 2, 3),
    (5, 5, 10),
    (-1, 1, 0),
    (0, 0, 0),
])
def test_add(a, b, expected):
    assert add(a, b) == expected

🔍 测试覆盖率

安装和运行

Bash
# 安装覆盖率插件
pip install pytest-cov

# 运行测试并生成覆盖率报告
pytest --cov=myproject --cov-report=html

# 查看终端报告
pytest --cov=myproject --cov-report=term-missing

覆盖率报告

Text Only
Name                    Stmts   Miss  Cover   Missing
-----------------------------------------------------
myproject/__init__.py       1      0   100%
myproject/utils.py         20      2    90%   15-16
myproject/main.py          30      5    83%   25-30
-----------------------------------------------------
TOTAL                      51      7    86%

💡 测试最佳实践

1. 测试命名规范

Python
# ✅ 好的命名
def test_calculate_average_with_valid_input():
    pass

def test_calculate_average_raises_error_for_empty_list():
    pass

# ❌ 不好的命名
def test1():
    pass

def test_func():
    pass

2. 一个测试只测一件事

Python
# ✅ 好的做法
def test_add_positive_numbers():
    assert add(1, 2) == 3

def test_add_negative_numbers():
    assert add(-1, -2) == -3

def test_add_mixed_numbers():
    assert add(-1, 1) == 0

# ❌ 不好的做法
def test_add():
    assert add(1, 2) == 3
    assert add(-1, -2) == -3
    assert add(-1, 1) == 0

3. 使用描述性的错误消息

Python
def test_user_has_permission():
    user = create_user(permissions=['read'])
    assert user.has_permission('read'), f"User permissions: {user.permissions}"

📝 练习

练习1: 为以下函数编写测试

Python
def is_palindrome(s):
    """判断字符串是否是回文"""
    s = s.lower().replace(" ", "")
    return s == s[::-1]

# 编写测试用例,考虑:
# - 普通回文
# - 非回文
# - 空字符串
# - 单个字符
# - 包含空格的回文

练习2: 测试异常

Python
def get_element(lst, index):
    """获取列表元素,索引越界时抛出IndexError"""
    if index < 0 or index >= len(lst):
        raise IndexError(f"Index {index} out of range")
    return lst[index]

# 编写测试,验证正常情况和异常情况

练习3: 使用Fixture

Python
import json

def load_config(filepath):
    """加载配置文件"""
    with open(filepath, 'r') as f:
        return json.load(f)

# 使用fixture创建临时配置文件进行测试

🎯 自我检查

  • 理解测试的重要性
  • 能编写基本的pytest测试
  • 会使用参数化测试
  • 会使用fixture管理测试资源
  • 能运行测试并查看覆盖率

📚 延伸阅读


下一步: 04 - 代码规范