3. unittest进阶:setUp与tearDown方法、setUpClass与tearDownClass、测试夹具(Fixture)管理、测试用例的组织与发现
好,咱们接着往下聊。上一章我们把 unittest 的基本用法过了一遍,你大概已经能写出简单的测试用例了。但说实话,真正写项目测试的时候,你会发现事情没那么简单——每个测试用例都要创建数据库连接、加载配置文件、初始化一些对象……如果每个用例都重复写一遍,那代码就太丑了。
这一章,我们就来解决这个问题。我会带你看看 unittest 里那些「让测试代码更干净」的利器。
3.1 setUp 与 tearDown:每个用例的前后处理
先说说最常用的两个方法:setUp 和 tearDown。
说白了,setUp 就是在每个测试用例执行之前自动运行的方法。tearDown 呢,就是每个用例执行之后自动运行的方法。它们俩就像一对门神,帮你把测试环境收拾得干干净净。
我举个例子你就明白了。假设我们要测试一个用户服务,每个测试用例都需要先创建一个数据库连接:
import unittest
class TestUserService(unittest.TestCase):
def setUp(self):
# 每个测试用例执行前都会调用
print("正在建立数据库连接...")
self.db = create_database_connection()
self.db.clear_all_data()
def tearDown(self):
# 每个测试用例执行后都会调用
print("正在关闭数据库连接...")
self.db.close()
def test_create_user(self):
user = self.db.create_user("张三", "zhangsan@example.com")
self.assertIsNotNone(user.id)
def test_delete_user(self):
self.db.create_user("李四", "lisi@example.com")
result = self.db.delete_user("lisi@example.com")
self.assertTrue(result)
你看,setUp 里做了两件事:建立连接、清空数据。tearDown 里关闭连接。这样每个测试用例开始的时候,数据库都是干干净净的,不会互相影响。
3.2 setUpClass 与 tearDownClass:类级别的前后处理
好,问题来了。如果每个测试用例都要创建数据库连接,那 100 个用例就要创建 100 次连接。这效率也太低了,对吧?
这时候就需要 setUpClass 和 tearDownClass 出场了。它们只在测试类开始前执行一次,结束后执行一次。注意,它们是类方法,需要用 @classmethod 装饰。
import unittest
class TestUserService(unittest.TestCase):
@classmethod
def setUpClass(cls):
# 整个测试类开始前执行一次
print("初始化数据库连接池...")
cls.db_pool = create_connection_pool()
@classmethod
def tearDownClass(cls):
# 整个测试类结束后执行一次
print("关闭数据库连接池...")
cls.db_pool.close_all()
def setUp(self):
# 每个用例前执行
self.conn = self.db_pool.get_connection()
self.conn.begin_transaction()
def tearDown(self):
# 每个用例后执行
self.conn.rollback()
self.conn.close()
def test_create_user(self):
user = self.conn.create_user("张三", "zhangsan@example.com")
self.assertIsNotNone(user.id)
这个模式在实际项目中非常常见。连接池只创建一次,每个用例拿一个连接,用完就还回去。我曾经在一个项目中看到有人把数据库连接创建写在 setUp 里,跑 500 个测试用例花了 3 分钟。改成 setUpClass 后,直接降到 10 秒。
3.3 测试夹具(Fixture)管理
聊到这儿,咱们得正式认识一下「测试夹具」这个概念。其实你刚才看到的 setUp、tearDown、setUpClass、tearDownClass,它们都属于测试夹具的范畴。
测试夹具,说白了就是「为了让测试能跑起来,需要提前准备好的东西」。比如:
- 数据库里的测试数据
- 临时文件
- 模拟的网络服务
- 环境变量
- 缓存数据
在 unittest 里,夹具的管理主要靠上面那四个方法。但实际项目中,我们还会遇到更复杂的情况。比如,有些夹具只在某些测试用例中需要,有些夹具需要嵌套使用。
我分享一个我常用的模式:
class TestOrderService(unittest.TestCase):
@classmethod
def setUpClass(cls):
# 全局夹具:数据库连接
cls.db = Database.connect()
def setUp(self):
# 每个用例的夹具:清空数据 + 创建基础数据
self.db.clear()
self.db.create_user("test_user")
self.db.create_product("test_product", price=100)
def test_create_order(self):
# 这个用例需要额外的夹具
self.db.add_balance("test_user", 500)
order = self.db.create_order("test_user", "test_product")
self.assertEqual(order.status, "paid")
def test_cancel_order(self):
# 先创建一个订单作为夹具
order = self.db.create_order("test_user", "test_product")
result = self.db.cancel_order(order.id)
self.assertTrue(result)
3.4 测试用例的组织与发现
好了,现在你有了很多测试用例。怎么组织它们?怎么让 unittest 自动找到它们并运行?
先说组织。我一般按模块来组织测试文件:
project/
├── src/
│ ├── user_service.py
│ ├── order_service.py
│ └── payment_service.py
└── tests/
├── __init__.py
├── test_user_service.py
├── test_order_service.py
└── test_payment_service.py
每个测试文件对应一个模块。文件名以 test_ 开头,这是 unittest 的默认发现规则。
再说发现。unittest 提供了 TestLoader 来自动发现测试用例:
# 方式一:命令行
# python -m unittest discover tests/
# 方式二:代码中手动加载
import unittest
loader = unittest.TestLoader()
suite = loader.discover('tests', pattern='test_*.py')
runner = unittest.TextTestRunner()
runner.run(suite)
discover 方法会递归查找 tests/ 目录下所有以 test_ 开头的文件,然后自动加载里面的测试类和方法。
这里有几个小细节要注意:
- 测试类必须继承
unittest.TestCase - 测试方法必须以
test开头 - 每个测试目录下最好放一个
__init__.py文件(可以是空的)
discover 就是找不到。查了半天,发现是文件名写成了 test-users.py,中间有个横线。unittest 默认只识别 test_*.py 这种下划线格式。嗯,这种小细节真的很坑。
3.5 综合示例:一个完整的测试夹具管理
最后,我给你看一个稍微完整点的例子。假设我们要测试一个博客系统的文章服务:
import unittest
import tempfile
import os
class TestArticleService(unittest.TestCase):
@classmethod
def setUpClass(cls):
# 创建临时目录作为测试存储
cls.temp_dir = tempfile.mkdtemp()
cls.service = ArticleService(storage_path=cls.temp_dir)
@classmethod
def tearDownClass(cls):
# 清理临时目录
import shutil
shutil.rmtree(cls.temp_dir)
def setUp(self):
# 每个用例前创建一篇默认文章
self.article = self.service.create_article(
title="测试文章",
content="这是测试内容"
)
def tearDown(self):
# 每个用例后清理所有文章
self.service.delete_all()
def test_update_article(self):
updated = self.service.update_article(
self.article.id,
title="更新后的标题"
)
self.assertEqual(updated.title, "更新后的标题")
def test_delete_article(self):
self.service.delete_article(self.article.id)
result = self.service.get_article(self.article.id)
self.assertIsNone(result)
这个例子把咱们今天讲的东西都用上了:
setUpClass创建临时目录(只做一次)tearDownClass清理临时目录(只做一次)setUp创建默认文章(每个用例都做)tearDown清理数据(每个用例都做)
这样设计的好处是:测试之间完全隔离,互不干扰。而且因为连接和目录只创建一次,性能也还不错。
好了,这一章的内容就到这儿。下一章我们会聊聊 mock 和 patch,那是处理外部依赖的利器。到时候你会发现,有了 mock,测试那些调第三方 API 的代码会变得特别轻松。