单元测试是写代码时不可或缺的一部分,尤其是用 Python 开发的时候。它能帮你确保代码按预期工作,发现 bug,还能让代码重构更安心。这篇文章会带你一步步搞懂 Python 单元测试,从基本概念到实际操作。
参考文章:Python 单元测试 | 简单一点学习 easyeasy.me
1. 什么是单元测试?
单元测试就是针对代码中最小的功能单元(通常是一个函数或方法)进行测试,确保它在各种输入下都能按预期工作。简单说,就是写点小测试,验证你的代码是不是靠谱。
- 为什么要做单元测试?
- 发现 bug:早点找到问题,改起来省心。
- 提高代码质量:写测试逼着你把代码写得更模块化。
- 便于重构:有测试兜底,改代码不怕翻车。
- 文档化:测试用例能让人快速看懂代码的功能。
- 单元测试 vs 其他测试
- 单元测试:只测单个函数或方法,隔离外部依赖(比如数据库、文件)。
- 集成测试:测多个组件一起工作的情况。
- 系统测试:测整个系统,接近真实使用场景。
接下来,我们从最简单的 Python 内置测试框架 unittest
开始。
2. Python 的 unittest 框架入门
Python 自带了一个叫 unittest
的模块,简单好用,适合写单元测试。我们先来看一个最基础的例子,假设你有个计算平方数的函数:
# math_utils.py def square(num): return num * num
怎么给这个函数写单元测试呢?下面是一个简单的测试代码:
# test_math_utils.py import unittest from math_utils import square class TestSquare(unittest.TestCase): def test_positive_number(self): result = square(3) self.assertEqual(result, 9) # 验证 3 的平方是 9 def test_negative_number(self): result = square(-2) self.assertEqual(result, 4) # 验证 -2 的平方是 4 def test_zero(self): result = square(0) self.assertEqual(result, 0) # 验证 0 的平方是 0 if __name__ == '__main__': unittest.main()
代码解析
- 导入
unittest
:这是 Python 内置的测试框架。 - 测试类:继承
unittest.TestCase
,每个测试类包含多个测试用例。 - 测试方法:以
test_
开头的方法是测试用例,比如test_positive_number
。 - 断言:用
self.assertEqual
检查结果是否符合预期。 - 运行测试:
unittest.main()
会自动运行所有测试用例。
运行测试
保存上面两个文件后,在命令行运行:
python -m unittest test_math_utils.py
输出会告诉你测试是否通过。如果通过,会看到类似:
... ---------------------------------------------------------------------- Ran 3 tests in 0.001s OK
如果有测试失败,会明确告诉你哪个测试用例出了问题。
3. 常用断言方法
unittest
提供了很多断言方法,帮你验证各种情况。以下是常用的几个:
断言方法 | 作用 |
assertEqual(a, b) |
检查 a == b |
assertNotEqual(a, b) |
检查 a != b |
assertTrue(x) |
检查 x 为 True |
assertFalse(x) |
检查 x 为 False |
assertIs(a, b) |
检查 a is b |
assertIsNone(x) |
检查 x is None |
assertIn(a, b) |
检查 a 在 b 中 |
assertRaises(Exception) |
检查代码是否抛出指定异常 |
断言示例
假设我们有个函数会抛出异常:
# math_utils.py def divide(a, b): if b == 0: raise ValueError("Cannot divide by zero") return a / b
对应的测试代码:
# test_math_utils.py import unittest from math_utils import divide class TestDivide(unittest.TestCase): def test_divide_by_zero(self): with self.assertRaises(ValueError): divide(10, 0) def test_divide_positive(self): self.assertEqual(divide(10, 2), 5.0)
这里用了 assertRaises
检查除以零时是否抛出 ValueError
。
4. 测试组织和最佳实践
写测试代码多了,你会发现需要点组织技巧,让代码更清晰,维护更省力。
4.1 测试文件和目录结构
- 测试文件命名:通常以
test_
开头,比如test_math_utils.py
。 - 目录结构:
project/ ├── src/ │ └── math_utils.py ├── tests/ │ └── test_math_utils.py
- 把测试代码和源代码分开,保持项目整洁。
4.2 setUp 和 tearDown
如果每个测试用例都需要初始化一些数据,可以用 setUp
和 tearDown
方法:
# test_math_utils.py import unittest from math_utils import square class TestSquare(unittest.TestCase): def setUp(self): print("Setting up for test") self.test_data = [2, -3, 0] def test_square_list(self): for num in self.test_data: result = square(num) self.assertEqual(result, num * num) def tearDown(self): print("Cleaning up after test")
setUp
:在每个测试用例运行前执行,适合初始化数据。tearDown
:在每个测试用例运行后执行,适合清理资源。
4.3 测试用例分组
可以用测试类把相关的测试分组。比如,TestSquare
和 TestDivide
可以写在同一个文件里,但分不同的类。
5. 使用 pytest 框架(更现代的选择)
虽然 unittest
是 Python 内置的,但 pytest
是个更现代、更简洁的测试框架,社区用得更多。安装 pytest:
pip install pytest
pytest 简单例子
还是测试 square
函数:
# test_math_utils_pytest.py from math_utils import square def test_positive_number(): assert square(3) == 9 def test_negative_number(): assert square(-2) == 4 def test_zero(): assert square(0) == 0
运行 pytest
pytest test_math_utils_pytest.py
pytest 的优势
- 更简洁:不用继承
TestCase
,直接写函数,断言用普通的assert
。 - 自动发现:运行
pytest
命令会自动找到所有以test_
开头的文件和函数。 - 丰富的插件:支持 mock、coverage 等功能。
- 详细的错误信息:失败时会清楚告诉你哪里不对。
pytest 常用功能
- Fixture(类似
setUp
):
import pytest fixture .def sample_data(): return [2, -3, 0] def test_square_list(sample_data): for num in sample_data: assert square(num) == num * num
- 参数化测试:
import pytest mark.parametrize("input,expected", [(3, 9), (-2, 4), (0, 0)]) .def test_square(input, expected): assert square(input) == expected
参数化测试能减少重复代码,特别适合测试多种输入。
6. Mock 和测试外部依赖
现实中,代码经常会依赖外部资源(比如数据库、API)。单元测试要求隔离这些依赖,这时候 unittest.mock
或 pytest 的 pytest-mock
就派上用场了。
Mock 示例
假设你有个函数调用外部 API:
# api_client.py import requests def get_user_data(user_id): response = requests.get(f"https://api.example.com/users/{user_id}") return response.json()
测试时不想真调用 API,可以用 mock:
# test_api_client.py from unittest.mock import Mock import pytest from api_client import get_user_data def test_get_user_data(mocker): # 模拟 requests.get mock_get = mocker.patch("requests.get") mock_get.return_value = Mock(json=lambda: {"id": 1, "name": "Alice"}) result = get_user_data(1) assert result == {"id": 1, "name": "Alice"} mock_get.assert_called_once_with("https://api.example.com/users/1")
这里用 mocker.patch
替换了 requests.get
,模拟返回一个假的响应。
7. 测试覆盖率
写完测试后,怎么知道测试覆盖了多少代码?可以用 pytest-cov
插件:
pip install pytest-cov pytest --cov=src tests/
这会告诉你哪些代码被测试覆盖,哪些没覆盖。目标是尽量提高覆盖率,但 100% 覆盖不一定现实,80%-90% 通常就够用了。
8. 常见问题和解决方案
- 测试运行太慢:检查是否 mock 了外部依赖,或者用
pytest
的--durations
参数找出慢的测试。 - 测试不稳定:可能是没正确隔离依赖,或者测试数据不一致。
- 测试代码太多:用参数化测试或 fixture 减少重复代码。
- 不知道测什么:优先测试核心逻辑、边界情况和异常处理。
9. 总结和进阶学习
单元测试是写好 Python 代码的必备技能。unittest
适合快速上手,pytest
更现代化,功能更强。建议:
- 小项目用
unittest
,简单直接。 - 中大型项目用
pytest
,配合插件更高效。 - 多写边界测试和异常测试,确保代码健壮。
- 学习 TDD(测试驱动开发),先写测试再写代码,体验会更好。