setUp和tearDown的增强版:setUpClass,tearDownClass,setUpModule,tearDownModule- 测试固件:
TestCase与TestSuite的组合艺术 - 参数化测试:用数据驱动测试
- Mocking(模拟):
unittest.mock的强大威力 - 测试跳过与预期失败:
skip和expectedFailure - 加载与发现测试:
TestLoader的自动化 - 测试运行器:
TextTestRunner与自定义输出 - 最佳实践与设计模式
setUp 和 tearDown 的增强版
标准的 setUp 和 tearDown 在每个测试方法执行前后都会运行,这在某些场景下效率不高。

setUpClass 和 tearDownClass
- 作用:在测试类的第一个测试方法运行前执行
setUpClass,在最后一个测试方法运行后执行tearDownClass。 - 用途:用于执行开销很大的操作,例如启动数据库连接、加载大型模型文件、创建临时服务器等,这样可以避免在每个测试中都重复这些操作。
- 注意:这两个方法必须是类方法,需要使用
@classmethod装饰器,并且第一个参数通常是cls。
import unittest
import time
class TestDatabaseConnection(unittest.TestCase):
@classmethod
def setUpClass(cls):
print("\n[Class] setUpClass: 正在建立数据库连接...")
# 模拟一个耗时的连接操作
time.sleep(1)
cls.db_connection = "Database Connection Established"
print("[Class] setUpClass: 数据库连接已建立。")
@classmethod
def tearDownClass(cls):
print("\n[Class] tearDownClass: 正在关闭数据库连接...")
# 模拟关闭连接
time.sleep(0.5)
cls.db_connection = None
print("[Class] tearDownClass: 数据库连接已关闭。")
def setUp(self):
print("\n[Method] setUp: 每个测试方法前准备测试数据...")
self.user_data = {"id": 1, "name": "Alice"}
def tearDown(self):
print("[Method] tearDown: 每个测试方法后清理测试数据...")
self.user_data = None
def test_create_user(self):
print("测试: 创建用户")
self.assertIsNotNone(self.db_connection)
self.assertEqual(self.db_connection, "Database Connection Established")
self.assertIn("name", self.user_data)
def test_get_user(self):
print("测试: 获取用户")
self.assertIsNotNone(self.db_connection)
self.assertEqual(self.user_data["name"], "Alice")
if __name__ == '__main__':
unittest.main()
setUpModule 和 tearDownModule
- 作用:在整个模块的所有测试开始前执行
setUpModule,在所有测试结束后执行tearDownModule。 - 用途:用于模块级别的设置和清理,例如创建和删除整个测试专用的临时目录。
# 在 test_example.py 文件的开头
def setUpModule():
print("\n[Module] setUpModule: 创建临时测试目录...")
# import os
# os.makedirs("temp_test_dir")
def tearDownModule():
print("\n[Module] tearDownModule: 删除临时测试目录...")
# import os
# import shutil
# shutil.rmtree("temp_test_dir")
# ... 你的 TestCase 类 ...
测试固件:TestCase 与 TestSuite 的组合艺术
当测试变多时,直接用 unittest.main() 会显得混乱。TestSuite 允许你将测试用例和测试套件组织起来,实现更精细的控制。
TestSuite:一个或多个测试用例的集合。TestLoader:用于从模块、类或文件中自动发现和加载测试,并构建TestSuite。
手动创建测试套件
# 假设你有两个测试类
class TestMathOperations(unittest.TestCase):
# ... 测试加法、减法等方法 ...
class TestStringOperations(unittest.TestCase):
# ... 测试拼接、分割等方法 ...
if __name__ == '__main__':
# 1. 创建一个测试套件
suite = unittest.TestSuite()
# 2. 将测试类或单个测试方法添加到套件中
# 添加整个测试类
suite.addTest(unittest.makeSuite(TestMathOperations))
# 添加单个测试方法
suite.addTest(TestStringOperations('test_concatenate'))
# 3. 创建一个测试运行器来运行套件
runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite)
自动发现测试
更常见的方式是让 TestLoader 自动发现你项目中的所有测试。
if __name__ == '__main__':
# 从当前目录开始,递归查找所有 test*.py 文件
loader = unittest.TestLoader()
suite = loader.discover(start_dir='.', pattern='test_*.py')
runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite)
最佳实践:将测试代码放在 tests 目录下,文件名以 test_ 开头,然后在项目根目录创建一个 run_tests.py 文件,内容如上所示,方便一键运行所有测试。
参数化测试
当一个测试逻辑相同,只是输入数据不同时,为了避免代码重复,我们可以使用参数化测试。

unittest 本身不直接支持参数化,但可以通过第三方库或一些技巧实现,这里介绍两种流行的方式。
使用 subTest (标准库,简单场景)
subTest 允许你在一个循环中运行多个子测试,如果其中一个失败,其他子测试仍会继续执行,并且报告会清晰地指出是哪个子测试失败了。
class TestListOperations(unittest.TestCase):
def test_sum_of_list_elements(self):
test_data = [
([1, 2, 3], 6),
([], 0),
([-1, 0, 1], 0),
([10, 20, 30], 60)
]
for input_list, expected_sum in test_data:
with self.subTest(input_list=input_list, expected_sum=expected_sum):
# 每次循环都会作为一个独立的测试用例来报告
self.assertEqual(sum(input_list), expected_sum)
# 运行时输出会类似这样:
# test_sum_of_list_elements (__main__.TestListOperations) ... ok
# test_sum_of_list_elements (__main__.TestListOperations) ... ok
# test_sum_of_list_elements (__main__.TestListOperations) ... ok
# test_sum_of_list_elements (__main__.TestListOperations) ... ok
使用 parameterized 库 (更强大)
parameterized 库是专门为参数化测试设计的,语法更简洁。
安装:
pip install parameterized

使用:
from parameterized import parameterized
import unittest
class TestStringMethods(unittest.TestCase):
@parameterized.expand([
("hello world", "hello", True),
("hello world", "hi", False),
("", "a", False),
("python", "PYTHON", False),
])
def test_substring(self, input_string, substring, expected):
self.assertEqual(substring in input_string, expected)
if __name__ == '__main__':
unittest.main(verbosity=2)
@parameterized.expand 会将列表中的每个元组转换成一个独立的测试方法,测试名称会包含参数信息,非常易于调试。
Mocking(模拟):unittest.mock 的强大威力
Mocking 是单元测试的核心技术之一,它的核心思想是:用一个“假”的对象(Mock Object)来替换掉被测代码所依赖的真实对象,这样,你可以:
- 隔离被测代码:测试
A时,不需要真的去连接数据库或调用外部 API,只需模拟数据库或 API 的行为即可。 - 控制测试环境:可以模拟各种边界条件和异常情况(如网络超时、API 返回 500 错误等)。
- 提升测试速度:避免了 I/O 操作(如网络请求、磁盘读写),测试运行得更快。
unittest.mock 模块提供了强大的 Mocking 工具,主要是 MagicMock 和 patch。
MagicMock
MagicMock 是一个通用的 Mock 对象,它会为你返回新的 MagicMock 实例作为任何属性的访问结果。
from unittest.mock import MagicMock
# 创建一个模拟对象
mock_connection = MagicMock()
# 模拟对象的方法调用
mock_connection.connect.return_value = "Connected!"
mock_connection.execute.return_value = {"id": 1, "name": "Mocked User"}
# 模拟对象的属性访问
mock_connection.timeout = 30
# 在你的代码中使用这个模拟对象
# db = get_db_connection() # 假设这返回了我们的 mock_connection
result = mock_connection.execute("SELECT * FROM users")
print(result) # 输出: {'id': 1, 'name': 'Mocked User'}
# 验证方法是否被以特定参数调用过
mock_connection.execute.assert_called_once_with("SELECT * FROM users")
# 验证方法是否被调用过
mock_connection.connect.assert_called()
patch (装饰器或上下文管理器)
patch 是最强大的工具,它可以在一个代码块内(通常是测试方法)临时替换一个对象(变量、方法、类等)。
语法: patch('target.object', new_mock_object)
最佳实践: target 应该是从被测模块的视角能够看到的导入路径。
示例:
假设我们有一个 emailer.py 模块和一个 app.py 模块。
emailer.py:
import smtplib
def send_email(to, subject, body):
with smtplib.SMTP('smtp.example.com', 587) as server:
server.sendmail('noreply@example.com', to, f"Subject: {subject}\n\n{body}")
print(f"Email sent to {to}")
app.py:
from emailer import send_email
def notify_user(user_email, message):
send_email(to=user_email, subject="You've got a message!", body=message)
我们想测试 app.py 中的 notify_user 函数,但不真的发送邮件。
test_app.py:
import unittest
from unittest.mock import patch, MagicMock
from app import notify_user
class TestApp(unittest.TestCase):
@patch('app.send_email') # 关键!从 app.py 的视角去 patch 'send_email'
def test_notify_user(self, mock_send_email):
# 1. 准备测试数据
test_email = "test@example.com"
test_message = "Hello, this is a test."
# 2. 调用被测函数
notify_user(test_email, test_message)
# 3. 断言
# 验证 send_email 是否被正确调用
mock_send_email.assert_called_once_with(
to=test_email,
subject="You've got a message!",
body=test_message
)
# 验证它没有被调用超过一次
mock_send_email.assert_called_once()
# 验证它没有被调用两次
# mock_send_email.assert_called_once() # 如果调用两次,这里会失败
if __name__ == '__main__':
unittest.main()
在这个例子中,@patch('app.send_email') 告诉 unittest:“在 test_notify_user 方法执行期间,把 app 模块里的 send_email 函数替换成一个 MagicMock 实例,并把那个实例作为参数 mock_send_email 传入。” 这样,notify_user 调用的 send_email 实际上就是我们的 mock_send_email,而不会真的发邮件。
测试跳过与预期失败
有时测试会因为某些原因无法运行,或者我们明确知道某个测试会失败,但希望它能被标记而不是直接导致测试套件失败。
@unittest.skip(reason): 无条件跳过测试。@unittest.skipIf(condition, reason): 如果条件为真,则跳过测试。@unittest.skipUnless(condition, reason): 除非条件为真,否则跳过测试。@unittest.expectedFailure: 标记一个测试为“预期失败”,如果测试真的失败了,它会被记录为“通过”(XPASS, expected pass);如果测试意外地通过了,它会被记录为“失败”(UPASS, unexpected pass)。
import sys
import unittest
class ConditionalTests(unittest.TestCase):
@unittest.skip("这个测试暂时不需要了")
def test_simple_skip(self):
self.fail("这个测试不应该被执行")
@unittest.skipIf(sys.version_info < (3, 7), "需要 Python 3.7 或更高版本")
def test_skip_on_version(self):
self.assertEqual("hello".upper(), "HELLO")
@unittest.skipUnless(sys.platform.startswith("win"), "仅适用于 Windows")
def test_windows_specific(self):
self.assertTrue(True) # 假设这是一个 Windows 特有的测试
@unittest.expectedFailure
def test_expected_failure(self):
self.assertEqual(1 + 1, 3) # 我们知道这个断言会失败
if __name__ == '__main__':
unittest.main()
加载与发现测试
这部分在 第2节 中已经有所涉及,但可以更深入。
unittest.TestLoader 负责加载测试。
loader.loadTestsFromName(name): 从一个给定的名称(如module.Class.test_method)加载测试。loader.loadTestsFromNames(names): 从多个名称加载测试。loader.loadTestsFromModule(module): 从一个模块中加载所有测试。loader.loadTestsFromTestCase(test_case_class):从一个测试类中加载所有测试。loader.discover(start_dir, pattern='test*.py'): 这是命令行python -m unittest背后使用的主要方法,它会递归地从start_dir目录下查找所有匹配pattern的文件,并从中加载测试。
测试运行器
unittest.TextTestRunner 是默认的文本运行器,你可以自定义它来改变输出格式或添加额外功能。
import unittest
from io import StringIO
# 自定义一个运行器,将输出重定向到 StringIO
class CustomTestRunner(unittest.TextTestRunner):
def __init__(self, stream=None, **kwargs):
if stream is None:
stream = StringIO() # 输出到内存,而不是控制台
super().__init__(stream=stream, **kwargs)
def get_output(self):
return self.stream.getvalue()
if __name__ == '__main__':
suite = unittest.TestLoader().loadTestsFromTestCase(TestListOperations)
runner = CustomTestRunner(verbosity=2)
result = runner.run(suite)
# 获取测试的文本输出
output = runner.get_output()
print("\n--- 获取到的测试输出 ---")
print(output)
更高级的用法是继承 unittest.TestRunner 来创建完全自定义的运行器,例如生成 HTML 报告、集成到 CI/CD 系统中并发送结果邮件等,对于 HTML 报告,推荐使用 pytest-html 或 unittest-xml-reporting 等第三方库。
最佳实践与设计模式
- 保持测试的独立性:每个测试都应该是独立的,不应该依赖于其他测试的执行顺序或结果。
setUp和tearDown是保证独立性的关键。 - AAA 模式 (Arrange, Act, Assert):
- Arrange (准备): 设置测试环境和数据。
- Act (执行): 调用被测试的方法或函数。
- Assert (断言): 验证结果是否符合预期。
- 测试应该快速:单元测试应该在几秒钟内完成,避免进行 I/O 操作,使用 Mock 来替代。
- 测试要具有描述性:测试方法的名称应该清晰地描述它正在测试什么。
test_user_login_with_valid_credentials比test_login更好。 - 不要测试私有方法:单元测试应该测试类的公共接口,测试私有方法通常意味着你的类设计可能存在问题(职责不够单一),如果确实需要,可以考虑重构代码或使用
@property将其变为公共方法。 - F.I.R.S.T. 原则:
- Fast (快速)
- Independent (独立)
- Repeatable (可重复)
- Self-Validating (自验证,不需要人工检查结果)
- Timely (及时,在编写代码的同时或之后立即编写测试)
通过掌握这些高级技巧,你的 unittest 测试将不再是简单的脚本,而是一个健壮、高效、易于维护的质量保障体系。
