Python 测试框架选型:unittest vs pytest,为什么我选择 pytest 作为主力框架
做自动化测试,第一个要面对的问题就是:选哪个框架?
Python 圈子里的测试框架不少,但真正能打的就两个——unittest 和 pytest。我入行那会儿,unittest 还是标配,几乎所有项目都在用。但后来我慢慢转向了 pytest,而且一用就是好几年。今天就跟大家聊聊,为什么我最终选了 pytest。
先说说 unittest 的问题
unittest 是 Python 标准库自带的,开箱即用,不用装任何第三方包。听起来很香对吧?但实际用起来,槽点不少。
第一,样板代码太多。 写一个测试类,必须继承 unittest.TestCase,每个方法都得写 self.assertEqual 这种冗长的调用。我见过一个项目,测试文件里一半都是模板代码,真正测逻辑的部分反而被淹没了。
第二,断言不够直观。 举个例子:
# unittest 风格
self.assertEqual(result, expected)
self.assertTrue(result > 0)
self.assertIn('error', response.text)
再看 pytest 的写法:
# pytest 风格
assert result == expected
assert result > 0
assert 'error' in response.text
哪个更清爽?一目了然。pytest 直接用 Python 原生的 assert,不需要记一堆 API。而且失败时,pytest 会智能地展示上下文信息,告诉你具体哪个值不对。
第三,fixture 管理太死板。 unittest 的 setUp 和 tearDown 只能在类级别或方法级别执行。如果你需要跨多个测试共享一个数据库连接,或者想按需加载数据,那代码会变得非常别扭。
核心差异速览:
| 对比维度 | unittest | pytest |
|---|---|---|
| 安装方式 | 内置,无需安装 | 需 pip install pytest |
| 断言语法 | self.assertXxx 系列 | 原生 assert |
| fixture 机制 | setUp/tearDown,作用域固定 | @pytest.fixture,作用域灵活 |
| 参数化 | subTest 或手动循环 | @pytest.mark.parametrize |
| 插件生态 | 弱 | 丰富(300+ 插件) |
| 并发执行 | 不支持 | pytest-xdist 支持 |
pytest 的杀手锏:fixture
fixture 是 pytest 最核心的设计之一。说白了,它就是用来准备测试环境和清理资源的。
我举个例子。假设你要测试一个用户注册接口,每次测试前需要创建一个临时用户,测试完再删掉。用 unittest 写,你得这样:
class TestUserAPI(unittest.TestCase):
def setUp(self):
self.user = create_test_user()
def tearDown(self):
delete_test_user(self.user)
def test_register(self):
# 测试逻辑
pass
def test_login(self):
# 测试逻辑
pass
看起来还行对吧?但问题来了——如果只有部分测试需要这个用户呢?或者你想让这个用户在整个测试模块里只创建一次?unittest 就有点力不从心了。
pytest 的 fixture 可以这样写:
@pytest.fixture
def test_user():
user = create_test_user()
yield user
delete_test_user(user)
def test_register(test_user):
# 直接用 test_user
pass
def test_login(test_user):
# 同样可以用
pass
而且 fixture 的作用域可以控制:function(每个函数)、class(每个类)、module(每个模块)、session(整个测试会话)。
我的经验: 在 CI 流水线里,我习惯把数据库连接、Redis 客户端、外部 API 的 mock 都做成 session 级别的 fixture。这样整个测试套件只初始化一次,跑起来快很多。但要注意——session 级别的 fixture 必须是线程安全的,否则并发跑测试时会出问题。
参数化测试:一个装饰器搞定
做接口测试时,经常需要测多组数据。比如测试登录接口,要测正确的账号、错误的密码、空字段、特殊字符……
用 unittest 写,要么写多个测试方法,要么用 subTest:
def test_login_multiple_cases(self):
cases = [
('admin', '123456', 200),
('admin', 'wrong', 401),
('', '123456', 400),
]
for username, password, expected in cases:
with self.subTest(username=username):
result = login(username, password)
self.assertEqual(result.status_code, expected)
这样写有个问题——如果中间某个 case 失败了,后面的 case 还会继续跑吗?不会。而且错误报告不够清晰,你不知道具体是哪个 case 挂了。
pytest 的参数化就优雅多了:
@pytest.mark.parametrize('username,password,expected', [
('admin', '123456', 200),
('admin', 'wrong', 401),
('', '123456', 400),
])
def test_login(username, password, expected):
result = login(username, password)
assert result.status_code == expected
每个 case 都是独立的测试用例,失败了也不影响其他 case。而且 pytest 会为每个 case 生成独立的测试报告,一眼就能看出哪个参数组合出了问题。
插件生态:让测试如虎添翼
pytest 的插件生态是我最看重的。说实话,unittest 在这方面几乎为零。
我常用的几个插件:
- pytest-xdist:并行执行测试。我有个项目有 2000 多个测试用例,单线程跑要 15 分钟,加上 xdist 后 4 个 worker 并行,直接降到 4 分钟。
- pytest-cov:覆盖率统计。和 CI 集成后,每次提交代码都能看到覆盖率变化。
- pytest-html:生成漂亮的 HTML 报告。给团队看、给领导汇报都很方便。
- pytest-mock:简化 mock 操作。比 unittest.mock 用起来顺手多了。
- pytest-timeout:给测试加超时限制。防止某个测试卡死导致整个 CI 挂掉。
避坑指南: 我曾经在一个项目里装了 20 多个 pytest 插件,结果测试启动时间从 2 秒变成了 10 秒。插件虽好,但别贪多。每个插件都会增加启动开销,建议只装真正需要的。
和 CI 的集成体验
pytest 和 CI 工具的集成非常丝滑。不管是 Jenkins、GitLab CI 还是 GitHub Actions,pytest 都能很好地配合。
举个例子,在 GitLab CI 里配置测试任务:
test:
script:
- pip install pytest pytest-cov
- pytest --cov=myapp --cov-report=term-missing --junitxml=report.xml
artifacts:
reports:
junit: report.xml
pytest 原生支持输出 JUnit XML 格式的报告,CI 工具可以直接解析。覆盖率报告也能直接展示在流水线页面上。
而 unittest 虽然也能输出 JUnit 格式,但需要额外装 unittest-xml-reporting,而且功能远不如 pytest 丰富。
什么时候我还会用 unittest?
说了这么多 pytest 的好话,但我也不是完全抛弃 unittest。有几种情况我仍然会用:
- 项目不允许装第三方包:有些安全要求极高的环境,只能使用标准库。
- 维护老项目:如果项目已经用 unittest 写了上千个测试,全部迁移成本太高,不如保持现状。
- 教学场景:unittest 的 API 设计更规范,适合教新手理解测试的基本概念。
但如果是新项目,我的选择永远是 pytest。没有之一。
一张图看懂我的选型逻辑
下面这张图总结了我选择 pytest 的核心考量:
总结一下
选 pytest 不是因为它花哨,而是因为它真的能提升效率。从写测试的速度、到维护的便利性、再到和 CI 的集成,pytest 都明显优于 unittest。
我见过太多团队,因为「unittest 是自带的不用装」这个理由一直用着,结果测试代码越来越臃肿,维护成本越来越高。其实装个 pytest 就一行命令的事,带来的收益却是长期的。
嗯,如果你还在犹豫,不妨拿一个小项目试试 pytest。写几个测试用例,感受一下 fixture 和参数化的便利。我相信你会跟我一样,回不去的。