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基础¶
安装¶
第一个测试¶
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
运行测试:
📝 编写测试¶
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 - 代码规范