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 选型决策树 Python 测试框架选型 unittest pytest 标准库内置 样板代码多 原生 assert 灵活 fixture 参数化 插件生态 CI 集成友好 并发执行 报告丰富 社区活跃 结论:新项目首选 pytest 老项目或受限环境可保留 unittest,但新项目建议直接上 pytest —— 这是我在多个项目中验证过的最佳实践

总结一下

选 pytest 不是因为它花哨,而是因为它真的能提升效率。从写测试的速度、到维护的便利性、再到和 CI 的集成,pytest 都明显优于 unittest。

我见过太多团队,因为「unittest 是自带的不用装」这个理由一直用着,结果测试代码越来越臃肿,维护成本越来越高。其实装个 pytest 就一行命令的事,带来的收益却是长期的。

嗯,如果你还在犹豫,不妨拿一个小项目试试 pytest。写几个测试用例,感受一下 fixture 和参数化的便利。我相信你会跟我一样,回不去的。