以下分别给出 unittest 和 pytest 的详细案例,包含常见测试场景:基本断言、异常测试、夹具、模拟、参数化、跳过/预期失败。
1. 被测试代码 (待测模块 calculator.py)
python
# calculator.py
import requests
class Calculator:
def add(self, a, b):
return a + b
def divide(self, a, b):
if b == 0:
raise ValueError("除数不能为零")
return a / b
def fetch_data(url):
"""模拟网络请求,返回状态码和JSON"""
resp = requests.get(url)
return resp.status_code, resp.json()
2. unittest 详细案例
python
# test_unittest_demo.py
import unittest
from unittest.mock import patch, Mock
from calculator import Calculator, fetch_data
class TestCalculator(unittest.TestCase):
"""基本测试 + 夹具 + 模拟 + 参数化 + 跳过"""
@classmethod
def setUpClass(cls):
"""所有测试运行前执行一次"""
print("\n[setUpClass] 初始化计算器实例")
cls.calc = Calculator()
@classmethod
def tearDownClass(cls):
"""所有测试运行后执行一次"""
print("[tearDownClass] 清理资源")
def setUp(self):
"""每个测试方法前执行"""
print(f"\n[setUp] 准备测试: {self._testMethodName}")
def tearDown(self):
"""每个测试方法后执行"""
print(f"[tearDown] 完成测试: {self._testMethodName}")
# ---------- 基本断言 ----------
def test_add_positive(self):
self.assertEqual(self.calc.add(2, 3), 5)
self.assertAlmostEqual(self.calc.add(0.1, 0.2), 0.3, places=6)
def test_divide_normal(self):
self.assertEqual(self.calc.divide(10, 2), 5.0)
# ---------- 异常测试 ----------
def test_divide_by_zero(self):
with self.assertRaises(ValueError) as ctx:
self.calc.divide(5, 0)
self.assertEqual(str(ctx.exception), "除数不能为零")
# ---------- 模拟 (mock) ----------
@patch('calculator.requests.get')
def test_fetch_data_success(self, mock_get):
# 配置模拟对象
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {"key": "value"}
mock_get.return_value = mock_response
status, data = fetch_data("http://example.com")
self.assertEqual(status, 200)
self.assertEqual(data, {"key": "value"})
mock_get.assert_called_once_with("http://example.com")
@patch('calculator.requests.get')
def test_fetch_data_failure(self, mock_get):
mock_response = Mock()
mock_response.status_code = 404
mock_response.json.side_effect = Exception("Not Found")
mock_get.return_value = mock_response
with self.assertRaises(Exception):
fetch_data("http://example.com/bad")
# ---------- 参数化测试 (使用 subTest) ----------
def test_add_multiple_inputs(self):
test_cases = [(1, 2, 3), (-1, -1, -2), (0, 0, 0), (100, -50, 50)]
for a, b, expected in test_cases:
with self.subTest(a=a, b=b, expected=expected):
self.assertEqual(self.calc.add(a, b), expected)
# ---------- 跳过测试 ----------
@unittest.skip("演示跳过,该功能尚未实现")
def test_multiply_not_implemented(self):
pass
@unittest.skipIf(True, "条件满足时跳过")
def test_skip_conditionally(self):
pass
@unittest.expectedFailure
def test_expected_failure(self):
# 这个断言会失败,但不会计入失败数
self.assertEqual(1, 2)
if __name__ == '__main__':
unittest.main(verbosity=2)
运行方式 :python test_unittest_demo.py 或 python -m unittest test_unittest_demo
3. pytest 详细案例
python
# test_pytest_demo.py
import pytest
from unittest.mock import Mock, patch
from calculator import Calculator, fetch_data
# ---------- 夹具 (fixture) ----------
@pytest.fixture(scope="class")
def calculator():
"""类级别夹具,返回计算器实例"""
print("\n[fixture] 创建计算器实例")
calc = Calculator()
yield calc
print("[fixture] 销毁计算器实例")
@pytest.fixture
def sample_data():
"""函数级别夹具,返回测试数据"""
return {"a": 10, "b": 5}
# ---------- 测试类 ----------
class TestCalculator:
"""使用类级别夹具"""
def test_add(self, calculator, sample_data):
result = calculator.add(sample_data["a"], sample_data["b"])
assert result == 15 # pytest 使用原生 assert
def test_divide_normal(self, calculator):
assert calculator.divide(9, 3) == 3.0
def test_divide_by_zero(self, calculator):
with pytest.raises(ValueError, match="除数不能为零"):
calculator.divide(5, 0)
# ---------- 参数化测试 (pytest 特色) ----------
@pytest.mark.parametrize("a,b,expected", [
(1, 2, 3),
(-1, -1, -2),
(0, 0, 0),
(100, -50, 50),
])
def test_add_parametrized(a, b, expected):
calc = Calculator()
assert calc.add(a, b) == expected
# 组合参数化 + 夹具
@pytest.mark.parametrize("input_val,expected", [(5, 5), (0, 0), (-3, -3)])
def test_identity(calculator, input_val, expected):
assert calculator.add(input_val, 0) == expected
# ---------- 模拟 (使用 mocker fixture,需要安装 pytest-mock) ----------
# 也可以直接使用 unittest.mock,pytest 自动兼容
def test_fetch_data_success(mocker):
# 使用 pytest-mock 的 mocker fixture
mock_get = mocker.patch('calculator.requests.get')
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {"key": "value"}
mock_get.return_value = mock_response
status, data = fetch_data("http://example.com")
assert status == 200
assert data == {"key": "value"}
mock_get.assert_called_once_with("http://example.com")
def test_fetch_data_failure():
with patch('calculator.requests.get') as mock_get:
mock_response = Mock()
mock_response.status_code = 404
mock_response.json.side_effect = Exception("Not Found")
mock_get.return_value = mock_response
with pytest.raises(Exception):
fetch_data("http://example.com/bad")
# ---------- 跳过与预期失败 ----------
@pytest.mark.skip(reason="演示跳过,未实现功能")
def test_skip_example():
pass
@pytest.mark.skipif(True, reason="条件满足时跳过")
def test_skip_conditionally():
pass
@pytest.mark.xfail(reason="已知问题,暂时预期失败")
def test_expected_failure():
assert 1 == 2
# ---------- 临时文件与 capsys (pytest 内置 fixture) ----------
def test_capsys_example(capsys):
print("Hello, world!")
captured = capsys.readouterr()
assert captured.out == "Hello, world!\n"
def test_tmp_path(tmp_path):
d = tmp_path / "sub"
d.mkdir()
f = d / "test.txt"
f.write_text("pytest rocks")
assert f.read_text() == "pytest rocks"
# ---------- 自定义标记 ----------
@pytest.mark.slow
def test_slow_operation():
import time
time.sleep(0.1)
assert True
# 运行方式: pytest -m slow (只运行标记为 slow 的测试)
运行方式:
- 安装 pytest:
pip install pytest pytest-mock - 执行:
pytest test_pytest_demo.py -v - 带覆盖率:
pytest --cov=calculator test_pytest_demo.py
关键差异对比
| 特性 | unittest | pytest |
|---|---|---|
| 断言风格 | self.assertEqual(a, b) |
assert a == b |
| 夹具 | setUp / tearDown |
@pytest.fixture,更灵活 |
| 参数化 | subTest 或 parameterized |
@pytest.mark.parametrize |
| 模拟 | unittest.mock |
unittest.mock 或 mocker |
| 跳过/预期失败 | @unittest.skip / expectedFailure |
@pytest.mark.skip / xfail |
| 插件生态 | 较少 | 极丰富 (xdist, cov, asyncio...) |
| 学习曲线 | 较低,但代码冗长 | 稍高,但更简洁强大 |
建议:新项目直接选择 pytest,旧项目维护可使用 unittest。两者可混合使用(pytest 能运行 unittest 风格的测试)。