核心概念
在学习具体用法前,先理解 unittest 的四个核心概念,这会让你更容易上手。

-
Test Case (测试用例)
- 定义: 测试的最小单元,一个测试用例通常用于测试一个特定的函数或方法是否能按预期工作。
- 实现: 继承
unittest.TestCase类,一个类中可以包含多个测试方法。 - 命名: 测试方法必须以
test_开头,unittest会自动发现并执行这些方法。
-
Test Suite (测试套件)
- 定义: 一个或多个测试用例的集合,你可以将相关的测试用例组织在一起,然后一次性运行整个套件。
- 实现: 可以手动创建,也可以使用
unittest的TestLoader自动从模块或目录中发现并加载测试用例来创建。
-
Test Runner (测试运行器)
- 定义: 负责执行测试套件,并收集测试结果,然后以某种格式(如文本、图形化)呈现出来。
- 实现:
unittest.TextTestRunner是最常用的文本运行器,你也可以自定义运行器或使用第三方库(如pytest)。
-
Test Fixture (测试夹具/固件)
(图片来源网络,侵删)- 定义: 在测试执行前后需要执行的操作,用于设置和清理测试环境,创建数据库连接、初始化测试数据、删除临时文件等。
- 实现:
setUp(): 在每个测试方法执行前被调用。tearDown(): 在每个测试方法执行后被调用。setUpClass(): 在一个测试类的所有测试方法执行前被调用(类方法,需使用@classmethod装饰器)。tearDownClass(): 在一个测试类的所有测试方法执行后被调用(类方法,需使用@classmethod装饰器)。
常用断言方法
断言是测试的核心,用于验证代码的实际行为是否符合预期。unittest.TestCase 类提供了丰富的断言方法。
| 断言方法 | 描述 | 示例 |
|---|---|---|
assertEqual(a, b) |
a == b |
self.assertEqual(1 + 1, 2) |
assertNotEqual(a, b) |
a != b |
self.assertNotEqual('hello', 'world') |
assertTrue(x) |
x is True |
self.assertTrue(isinstance(1, int)) |
assertFalse(x) |
x is False |
self.assertFalse(None) |
assertIs(a, b) |
a is b (身份判断) |
self.assertIs(None, None) |
assertIsNot(a, b) |
a is not b |
self.assertIsNot([], []) |
assertIsNone(x) |
x is None |
self.assertIsNone(obj.value) |
assertIsNotNone(x) |
x is not None |
self.assertIsNotNone(obj) |
assertIn(a, b) |
a in b |
self.assertIn('a', 'apple') |
assertNotIn(a, b) |
a not in b |
self.assertNotIn('z', 'apple') |
assertIsInstance(a, b) |
isinstance(a, b) |
self.assertIsInstance([], list) |
assertNotIsInstance(a, b) |
not isinstance(a, b) |
self.assertNotIsInstance([], dict) |
assertRaises(Exception, func, *args, **kwargs) |
func 执行时是否抛出指定的异常 |
with self.assertRaises(ValueError): int('a') |
assertAlmostEqual(a, b, places=7) |
round(a-b, places) == 0 (浮点数近似相等) |
self.assertAlmostEqual(0.1 + 0.2, 0.3) |
assertNotAlmostEqual(a, b, places=7) |
round(a-b, places) != 0 |
self.assertNotAlmostEqual(0.1, 0.2) |
一个完整的示例
让我们通过一个例子来串联所有概念,假设我们有一个简单的 calculator.py 文件,我们要测试它的功能。
待测试的代码 (calculator.py)
# 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("Cannot divide by zero")
return a / b
编写测试用例 (test_calculator.py)
# test_calculator.py
import unittest
from calculator import Calculator
class TestCalculator(unittest.TestCase):
"""测试 Calculator 类"""
def setUp(self):
"""在每个测试方法前执行,用于初始化"""
self.calc = Calculator()
print("\n--- Setting up a new calculator instance ---")
def tearDown(self):
"""在每个测试方法后执行,用于清理"""
print("--- Calculator instance torn down ---")
@classmethod
def setUpClass(cls):
"""在所有测试方法前执行一次"""
print("\n=== Starting Calculator Tests ===")
@classmethod
def tearDownClass(cls):
"""在所有测试方法后执行一次"""
print("=== Finished Calculator Tests ===")
def test_add(self):
"""测试加法功能"""
self.assertEqual(self.calc.add(1, 2), 3)
self.assertEqual(self.calc.add(-1, 5), 4)
self.assertEqual(self.calc.add(0, 0), 0)
def test_subtract(self):
"""测试减法功能"""
self.assertEqual(self.calc.subtract(5, 3), 2)
self.assertEqual(self.calc.subtract(3, 5), -2)
def test_multiply(self):
"""测试乘法功能"""
self.assertEqual(self.calc.multiply(3, 4), 12)
self.assertEqual(self.calc.multiply(-2, 5), -10)
def test_divide(self):
"""测试除法功能"""
self.assertEqual(self.calc.divide(10, 2), 5.0)
self.assertEqual(self.calc.divide(5, 2), 2.5)
# 测试除以零的情况
with self.assertRaises(ValueError):
self.calc.divide(10, 0)
if __name__ == '__main__':
# 运行测试
unittest.main()
如何运行测试
直接在文件末尾运行
python test_calculator.py
使用 unittest 模块命令行

# 运行指定模块的所有测试 python -m unittest test_calculator # 运行指定模块中的指定测试类 python -m unittest test_calculator.TestCalculator # 运行指定模块中的指定测试方法 python -m unittest test_calculator.TestCalculator.test_add
使用 unittest.TestLoader 自动发现并运行
如果你的测试文件遵循 test_*.py 的命名规范,并且测试类遵循 Test* 的命名规范,你可以让 unittest 自动发现所有测试。
# runner.py
import unittest
if __name__ == '__main__':
# 创建一个测试套件
suite = unittest.TestLoader().discover('tests', pattern='test_*.py')
# 创建一个测试运行器
runner = unittest.TextTestRunner(verbosity=2) # verbosity=2 显示更详细的信息
# 运行测试套件
runner.run(suite)
假设你的测试文件都放在一个名为 tests 的目录下,运行 python runner.py 即可。
运行结果分析
当你运行 python test_calculator.py 时,会看到类似下面的输出:
=== Starting Calculator Tests ===
--- Setting up a new calculator instance ---
--- Calculator instance torn down ---
--- Setting up a new calculator instance ---
.test_subtract (test_calculator.TestCalculator) ... ok
--- Calculator instance torn down ---
--- Setting up a new calculator instance ---
.test_multiply (test_calculator.TestCalculator) ... ok
--- Calculator instance torn down ---
--- Setting up a new calculator instance ---
.test_divide (test_calculator.TestCalculator) ... ok
--- Calculator instance torn down ---
--- Setting up a new calculator instance ---
.test_add (test_calculator.TestCalculator) ... ok
--- Calculator instance torn down ---
----------------------------------------------------------------------
Ran 4 tests in 0.001s
OK
OK: 所有测试通过。F(Failure): 断言失败 (assertEqual等),测试代码逻辑正确,但被测代码行为不符合预期。E(Error): 测试代码本身抛出了未捕获的异常(比如语法错误、调用了不存在的方法等)。
高级用法与最佳实践
跳过测试
有时你希望暂时跳过某个测试,或者只在特定条件下运行它。
@unittest.skip(reason): 强制跳过测试。@unittest.skipIf(condition, reason): 如果条件为真,则跳过。@unittest.skipUnless(condition, reason): 除非条件为真,否则跳过。
import sys
@unittest.skip("This test is deprecated")
def test_old_feature(self):
pass
@unittest.skipIf(sys.version_info < (3, 7), "This test requires Python 3.7+")
def test_new_feature(self):
pass
setUp 和 tearDown 的使用场景
setUp: 创建数据库连接、打开文件、初始化共享对象、设置环境变量等。tearDown: 关闭数据库连接、删除临时文件、恢复环境变量等。
重要: setUp 和 tearDown 是每个测试方法都会执行一次,如果你只需要在测试类开始/结束时执行一次,请使用 setUpClass 和 tearDownClass。
使用 Mock 对象
当你需要测试的代码依赖于外部系统(如网络请求、数据库、文件系统)时,使用真实的依赖会让测试变得缓慢且不可靠,这时就需要 Mock 对象。
unittest.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}")
response.raise_for_status() # 如果请求失败则抛出异常
return response.json()
# test_api_client.py
from unittest.mock import patch
from api_client import get_user_data
import unittest
class TestApiClient(unittest.TestCase):
@patch('api_client.requests.get') # 用 Mock 对象替换 'api_client' 模块中的 'requests.get'
def test_get_user_data_success(self, mock_get):
# 1. 配置 Mock 对象的行为
mock_response = unittest.mock.Mock()
mock_response.json.return_value = {'id': 1, 'name': 'Alice'}
mock_response.raise_for_status.return_value = None
mock_get.return_value = mock_response
# 2. 执行被测代码
data = get_user_data(1)
# 3. 断言
self.assertEqual(data, {'id': 1, 'name': 'Alice'})
# 验证 requests.get 是否被正确调用
mock_get.assert_called_once_with("https://api.example.com/users/1")
@patch('api_client.requests.get')
def test_get_user_data_failure(self, mock_get):
# 配置 Mock 对象在调用时抛出异常
mock_get.side_effect = requests.exceptions.HTTPError("404 Not Found")
# 断言调用会抛出异常
with self.assertRaises(requests.exceptions.HTTPError):
get_user_data(99)
测试套件的组织
随着项目变大,测试文件会越来越多,建议将测试文件放在与项目根目录平行的 tests 目录下,并保持与源代码相同的目录结构。
my_project/
├── src/
│ └── my_module/
│ ├── __init__.py
│ └── calculator.py
tests/
├── __init__.py
└── test_calculator.py
然后使用 unittest.TestLoader().discover() 来自动发现和运行所有测试。
总结与对比
| 特性 | unittest |
pytest |
|---|---|---|
| 类型 | Python 内置标准库 | 第三方库 |
| 学习曲线 | 较陡峭,需要理解类、套件、运行器等概念 | 非常平缓,遵循 "约定优于配置" |
| 断言 | 提供专门的断言方法 (e.g., assertEqual) |
直接使用 Python 的 assert 语句,失败信息更清晰 |
| Fixture | setUp, tearDown, @classmethod 装饰器 |
使用 @pytest.fixture 装饰器,功能更强大灵活 |
| 发现测试 | 需要手动创建 TestSuite 或使用 TestLoader |
自动发现 test_*.py 文件中的 test_* 函数 |
| 参数化 | 需要继承 unittest.TestCase 并使用子类,比较麻烦 |
使用 @pytest.mark.parametrize 装饰器,非常方便 |
| Mock | unittest.mock 模块 |
unittest.mock (pytest 内置支持) 或 pytest-mock 插件 |
| 生态 | 标准库,无需安装 | 社区庞大,插件丰富,功能强大 |
unittest: 适合简单的项目,或者作为学习测试框架的入门选择,如果你已经在使用 Python 标准库,它是一个可靠的选择。pytest: 更现代、更强大、更易用,是目前 Python 社区的主流选择,它兼容unittest,可以无缝运行unittest的测试代码,并提供了许多高级功能,强烈推荐学习和使用。
希望这份详细的指南能帮助你掌握 Python unittest 的常用功能!
