Python单元测试实现指南
基本单元测试结构
# calculator.py - 待测试的模块
class Calculator:
def add(self, a, b):
return a + b
def subtract(self, a, b):
return a - b
def multiply(self, a, b):
return a * b
def divide(self, a, b):
if b == 0:
raise ValueError("除数不能为0")
return a / b
使用unittest框架
# test_calculator.py
import unittest
from calculator import Calculator
class TestCalculator(unittest.TestCase):
def setUp(self):
"""每个测试方法执行前都会调用"""
self.calc = Calculator()
def test_add(self):
"""测试加法"""
result = self.calc.add(3, 5)
self.assertEqual(result, 8)
self.assertEqual(self.calc.add(-1, 1), 0)
self.assertEqual(self.calc.add(0, 0), 0)
def test_subtract(self):
"""测试减法"""
self.assertEqual(self.calc.subtract(10, 5), 5)
self.assertEqual(self.calc.subtract(5, 10), -5)
def test_multiply(self):
"""测试乘法"""
self.assertEqual(self.calc.multiply(3, 4), 12)
self.assertEqual(self.calc.multiply(-2, 3), -6)
self.assertEqual(self.calc.multiply(0, 5), 0)
def test_divide(self):
"""测试除法"""
self.assertEqual(self.calc.divide(10, 2), 5)
self.assertEqual(self.calc.divide(7, 2), 3.5)
with self.assertRaises(ValueError):
self.calc.divide(10, 0)
def test_divide_by_zero(self):
"""测试除以零异常"""
with self.assertRaises(ValueError) as context:
self.calc.divide(10, 0)
self.assertEqual(str(context.exception), "除数不能为0")
if __name__ == '__main__':
unittest.main()
使用pytest框架(推荐)
# test_calculator_pytest.py
import pytest
from calculator import Calculator
@pytest.fixture
def calculator():
"""fixture提供测试环境"""
return Calculator()
def test_add(calculator):
assert calculator.add(3, 5) == 8
assert calculator.add(-1, 1) == 0
assert calculator.add(0, 0) == 0
def test_subtract(calculator):
assert calculator.subtract(10, 5) == 5
assert calculator.subtract(5, 10) == -5
def test_multiply(calculator):
assert calculator.multiply(3, 4) == 12
assert calculator.multiply(-2, 3) == -6
assert calculator.multiply(0, 5) == 0
def test_divide(calculator):
assert calculator.divide(10, 2) == 5
assert calculator.divide(7, 2) == 3.5
def test_divide_by_zero(calculator):
with pytest.raises(ValueError, match="除数不能为0"):
calculator.divide(10, 0)
测试复杂场景
# user_service.py
class UserService:
def __init__(self, database):
self.database = database
def create_user(self, username, email):
if not username or not email:
raise ValueError("用户名和邮箱不能为空")
if self.database.exists(username):
raise ValueError("用户已存在")
user = {
'username': username,
'email': email,
'active': True
}
return self.database.save(user)
def get_user(self, username):
user = self.database.find(username)
if not user:
return None
return user
def deactivate_user(self, username):
user = self.get_user(username)
if not user:
raise ValueError("用户不存在")
user['active'] = False
return self.database.update(user)
# test_user_service.py
import unittest
from unittest.mock import Mock, patch
from user_service import UserService
class TestUserService(unittest.TestCase):
def setUp(self):
self.mock_database = Mock()
self.service = UserService(self.mock_database)
def test_create_user_success(self):
"""测试成功创建用户"""
self.mock_database.exists.return_value = False
self.mock_database.save.return_value = {
'username': 'testuser',
'email': 'test@example.com',
'active': True
}
result = self.service.create_user('testuser', 'test@example.com')
self.assertEqual(result['username'], 'testuser')
self.assertTrue(result['active'])
self.mock_database.exists.assert_called_once_with('testuser')
self.mock_database.save.assert_called_once()
def test_create_user_exists(self):
"""测试创建已存在的用户"""
self.mock_database.exists.return_value = True
with self.assertRaises(ValueError) as context:
self.service.create_user('existing', 'test@example.com')
self.assertEqual(str(context.exception), "用户已存在")
def test_create_user_empty_input(self):
"""测试空输入"""
with self.assertRaises(ValueError):
self.service.create_user('', 'test@example.com')
with self.assertRaises(ValueError):
self.service.create_user('testuser', '')
def test_get_user_found(self):
"""测试获取存在的用户"""
expected_user = {'username': 'testuser', 'email': 'test@example.com'}
self.mock_database.find.return_value = expected_user
result = self.service.get_user('testuser')
self.assertEqual(result, expected_user)
self.mock_database.find.assert_called_once_with('testuser')
def test_get_user_not_found(self):
"""测试获取不存在的用户"""
self.mock_database.find.return_value = None
result = self.service.get_user('nonexistent')
self.assertIsNone(result)
def test_deactivate_user(self):
"""测试停用用户"""
user = {'username': 'testuser', 'active': True}
self.mock_database.find.return_value = user
self.mock_database.update.return_value = {**user, 'active': False}
result = self.service.deactivate_user('testuser')
self.assertFalse(result['active'])
self.mock_database.update.assert_called_once()
if __name__ == '__main__':
unittest.main()
运行测试
# 运行所有测试
python -m unittest discover
# 运行特定测试文件
python -m unittest test_calculator.py
# 运行特定测试类
python -m unittest test_calculator.TestCalculator
# 运行特定测试方法
python -m unittest test_calculator.TestCalculator.test_add
# 使用 pytest 运行
pytest test_calculator_pytest.py -v
# 生成覆盖率报告
pip install coverage
coverage run -m pytest
coverage report -m
实用的断言方法
import unittest
class TestAssertions(unittest.TestCase):
def test_common_assertions(self):
# 相等/不等
self.assertEqual(1 + 1, 2)
self.assertNotEqual(1 + 1, 3)
# 布尔值
self.assertTrue(True)
self.assertFalse(False)
# 空值
self.assertIsNone(None)
self.assertIsNotNone("hello")
# 类型
self.assertIsInstance(42, int)
self.assertNotIsInstance("42", int)
# 列表/元组
self.assertIn("a", ["a", "b", "c"])
self.assertNotIn("x", ["a", "b", "c"])
# 异常
with self.assertRaises(ValueError):
int("abc")
# 近似相等(浮点数)
self.assertAlmostEqual(0.1 + 0.2, 0.3, places=7)
测试最佳实践
# test_best_practices.py
"""
最佳实践示例:
1. 测试方法命名规范:test_{function_name}_{scenario}
2. 每个测试方法只测试一个功能点
3. 使用 setUp 和 tearDown 管理测试资源
4. 测试边界条件
5. 使用 mock 隔离外部依赖
"""
import unittest
from unittest.mock import patch, MagicMock
import requests
class DataProcessor:
def fetch_and_process(self, url):
response = requests.get(url)
if response.status_code == 200:
data = response.json()
return self._process(data)
return None
def _process(self, data):
return [item['name'] for item in data if item.get('active')]
class TestDataProcessor(unittest.TestCase):
@patch('requests.get')
def test_fetch_and_process_success(self, mock_get):
"""测试成功获取并处理数据"""
# 准备 mock 数据
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = [
{'name': 'item1', 'active': True},
{'name': 'item2', 'active': False},
{'name': 'item3', 'active': True}
]
mock_get.return_value = mock_response
# 执行测试
processor = DataProcessor()
result = processor.fetch_and_process('http://example.com/api')
# 验证结果
self.assertEqual(result, ['item1', 'item3'])
mock_get.assert_called_once_with('http://example.com/api')
@patch('requests.get')
def test_fetch_and_process_failure(self, mock_get):
"""测试请求失败"""
mock_response = MagicMock()
mock_response.status_code = 404
mock_get.return_value = mock_response
processor = DataProcessor()
result = processor.fetch_and_process('http://example.com/api')
self.assertIsNone(result)
if __name__ == '__main__':
unittest.main()
- 选择测试框架:
unittest(内置)或 pytest(更简洁)
- 测试结构:遵循 AAA 模式(Arrange-Act-Assert)
- 命名规范:
test_功能_场景
- 隔离测试:使用 mock 对象模拟外部依赖
- 覆盖率:关注代码覆盖率和边界情况
- 持续集成:将测试集成到 CI/CD 流程中
