Python单元测试与测试驱动开发:从入门到实践
·
Python单元测试与测试驱动开发:从入门到实践
引言
测试驱动开发(TDD)是一种软件开发方法论,强调在编写实际代码之前先编写测试。Python提供了强大的测试框架支持,使得TDD成为一种高效的开发方式。
本文将深入探讨Python单元测试的核心概念,并分享TDD的最佳实践。
一、单元测试基础
1.1 使用unittest模块
import unittest
class TestMathOperations(unittest.TestCase):
def setUp(self):
"""测试前置条件"""
self.base = 10
def tearDown(self):
"""测试清理工作"""
pass
def test_addition(self):
"""测试加法运算"""
result = self.base + 5
self.assertEqual(result, 15)
def test_subtraction(self):
"""测试减法运算"""
result = self.base - 3
self.assertTrue(result == 7)
def test_multiplication(self):
"""测试乘法运算"""
result = self.base * 2
self.assertNotEqual(result, 19)
if __name__ == '__main__':
unittest.main()
1.2 使用pytest框架
import pytest
def add(a, b):
return a + b
def test_add_positive_numbers():
assert add(2, 3) == 5
def test_add_negative_numbers():
assert add(-1, -1) == -2
def test_add_with_zero():
assert add(0, 0) == 0
@pytest.mark.parametrize("a, b, expected", [
(1, 2, 3),
(-1, 1, 0),
(0, 0, 0),
(10, 20, 30),
])
def test_add_multiple_cases(a, b, expected):
assert add(a, b) == expected
二、测试驱动开发流程
2.1 TDD三步法
# 步骤1: 编写失败的测试
def test_calculate_discount():
"""测试折扣计算"""
price = 100
discount = 0.2
expected = 80
assert calculate_discount(price, discount) == expected
# 步骤2: 编写最小代码使测试通过
def calculate_discount(price, discount):
return price * (1 - discount)
# 步骤3: 重构代码
def calculate_discount(price: float, discount: float) -> float:
"""计算折扣后价格"""
if discount < 0 or discount > 1:
raise ValueError("折扣必须在0到1之间")
return price * (1 - discount)
2.2 TDD实战示例
# 需求:实现一个栈数据结构
class TestStack:
def test_stack_is_empty_initially(self):
stack = Stack()
assert stack.is_empty()
def test_push_adds_element(self):
stack = Stack()
stack.push(1)
assert not stack.is_empty()
def test_pop_returns_last_element(self):
stack = Stack()
stack.push(1)
stack.push(2)
assert stack.pop() == 2
def test_pop_from_empty_stack_raises_error(self):
stack = Stack()
with pytest.raises(IndexError):
stack.pop()
# 实现栈
class Stack:
def __init__(self):
self.items = []
def is_empty(self):
return len(self.items) == 0
def push(self, item):
self.items.append(item)
def pop(self):
if self.is_empty():
raise IndexError("Cannot pop from empty stack")
return self.items.pop()
三、高级测试技术
3.1 Mock对象
from unittest.mock import Mock, patch
def test_api_call(mocker):
# 创建mock对象
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {"data": "test"}
# 替换requests.get
with patch('requests.get', return_value=mock_response):
result = fetch_data('https://api.example.com')
assert result == {"data": "test"}
requests.get.assert_called_once_with('https://api.example.com')
3.2 Fixture机制
import pytest
@pytest.fixture
def database_connection():
"""创建数据库连接fixture"""
conn = create_connection()
yield conn
conn.close()
@pytest.fixture
def test_user():
"""创建测试用户fixture"""
user = User(name="Test", email="test@example.com")
return user
def test_user_creation(database_connection, test_user):
"""测试用户创建"""
database_connection.save(test_user)
retrieved = database_connection.get_by_id(test_user.id)
assert retrieved.name == "Test"
3.3 参数化测试
import pytest
@pytest.mark.parametrize(
"input_data, expected",
[
("hello", "HELLO"),
("world", "WORLD"),
("Python", "PYTHON"),
],
ids=["lowercase", "mixed_case", "title_case"]
)
def test_string_upper(input_data, expected):
assert input_data.upper() == expected
@pytest.mark.parametrize("value", [1, 2, 3, 4, 5])
def test_positive_numbers(value):
assert value > 0
四、测试覆盖率
4.1 使用coverage.py
# 安装coverage
pip install coverage
# 运行测试并生成报告
coverage run -m pytest tests/
coverage report -m
coverage html
4.2 覆盖率目标
# .coveragerc配置文件
[run]
source = .
omit =
*/tests/*
*/__init__.py
[report]
show_missing = True
fail_under = 80
五、测试最佳实践
5.1 测试命名规范
# 好的测试命名
def test_user_can_login_with_valid_credentials():
pass
def test_user_cannot_login_with_invalid_password():
pass
def test_api_returns_404_for_nonexistent_resource():
pass
# 避免的命名
def test_login(): # 太模糊
pass
def test_case_1(): # 没有描述性
pass
5.2 测试隔离
def test_database_transactions():
"""每个测试应该独立"""
# 在测试开始时清理状态
clear_database()
# 执行测试
create_user("test")
# 验证结果
assert get_user_count() == 1
def clear_database():
"""清理数据库"""
# 删除所有数据
pass
5.3 测试文档化
def test_checkout_process_with_insufficient_stock():
"""
测试库存不足时的结账流程
场景:用户尝试购买库存不足的商品
预期:系统应返回错误信息,订单不应创建
"""
product = Product(name="Book", stock=0)
cart = Cart(items=[product])
with pytest.raises(InsufficientStockError):
checkout(cart)
六、持续集成中的测试
6.1 GitHub Actions配置
# .github/workflows/tests.yml
name: Run Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.10'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest pytest-mock coverage
pip install -e .
- name: Run tests
run: pytest tests/ -v
- name: Check coverage
run: coverage run -m pytest tests/ && coverage report --fail-under=80
6.2 测试报告集成
# 生成JUnit格式报告
pytest tests/ --junitxml=results.xml
# 发送测试结果到Slack
def notify_slack(results):
message = f"测试完成: {results.passed}/{results.total} 通过"
send_slack_message(message)
七、总结
TDD的优势:
- 更早发现问题:在开发早期捕获bug
- 更好的设计:测试驱动产生更清晰的API
- 文档作用:测试用例作为活文档
- 重构信心:测试确保重构不会破坏功能
在实际项目中,建议:
- 采用TDD方法论
- 保持测试简洁独立
- 追求合理的测试覆盖率
- 集成到CI/CD流程中
思考:在你的项目中,TDD带来了哪些好处?欢迎分享!
更多推荐


所有评论(0)