pytest 相比 unittest 有什么优势?常用特性有哪些?
简化版
pytest 是 Python 生态里非常常用的测试框架,相比 unittest 写法更简洁、断言更自然、fixture 更强大、插件生态更丰富。它既能运行普通函数式测试,也兼容 unittest 风格测试,适合从小项目到大型工程逐步演进。
详细版
pytest 的优势主要体现在几个方面:
- 用普通函数就能写测试,不强制继承
TestCase; - 直接使用 Python 的
assert,失败时会展示更详细的表达式差异; - fixture 可以优雅管理测试前置资源和清理逻辑;
- 支持参数化测试,减少重复用例;
- 插件生态丰富,例如覆盖率、并发执行、Django/FastAPI 测试集成;
- 能自动发现
test_*.py、*_test.py、test_*函数或方法; - 可以通过 marker 对测试分组,例如 smoke、slow、integration。
示例:
def add(a, b):
return a + b
def test_add():
assert add(1, 2) == 3
面试中可以强调:pytest 不是只让语法变短,它真正提升的是测试组织、复用和工程化能力。
完整版教学
一、为什么 Python 项目常用 pytest
Python 标准库自带 unittest,它是 xUnit 风格框架,和 Java JUnit 很像:测试类继承 unittest.TestCase,通过 self.assertEqual、self.assertTrue 等方法断言。它稳定、内置、兼容性好,但写起来相对啰嗦。
pytest 的思路更 Pythonic。测试可以只是一个普通函数,断言可以直接使用 Python 原生 assert:
def test_user_name():
user = {"name": "Tom"}
assert user["name"] == "Tom"
这降低了写测试的心理成本。测试写得越自然,团队越愿意补测试;团队愿意补测试,项目质量才会真正上去。
二、pytest 的测试发现规则
pytest 默认会从当前目录递归查找测试文件。常见规则是文件名以 test_ 开头或以 _test.py 结尾,测试函数以 test_ 开头,测试类以 Test 开头。
例如:
tests/
test_user_service.py
test_order_api.py
文件里可以写:
def test_create_user():
...
class TestOrderService:
def test_create_order(self):
...
这套规则让测试结构有明确约定。工程里最好把测试放在 tests/ 目录,按业务模块或层次拆分,不要散落在各处。
三、assert 重写为什么好用
pytest 最让人舒服的地方之一是断言失败信息清楚。比如:
def test_list():
assert [1, 2, 3] == [1, 2, 4]
pytest 会展示两个列表在哪个位置不同,而不是只告诉你 False is not true。这是因为 pytest 会对 assert 语句做断言重写,记录表达式中间值。
这点看似小,但排查失败用例时很有价值。测试失败不可怕,看不懂失败原因才可怕。
四、fixture 是 pytest 的核心能力
很多测试都需要前置资源,例如数据库连接、临时文件、登录用户、测试客户端。unittest 常用 setUp 和 tearDown,pytest 更推荐 fixture:
import pytest
@pytest.fixture
def user():
return {"id": 1, "name": "Tom"}
def test_user_name(user):
assert user["name"] == "Tom"
测试函数只要声明参数 user,pytest 就会自动找到同名 fixture 并注入。fixture 还能设置作用域、依赖其他 fixture,并用 yield 做清理。
这让测试前置逻辑可以模块化复用,而不是在每个测试类里重复写初始化代码。
五、参数化测试减少重复
如果一个函数需要测试多组输入输出,pytest 可以用参数化:
import pytest
@pytest.mark.parametrize(
"a,b,expected",
[
(1, 2, 3),
(0, 0, 0),
(-1, 1, 0),
],
)
def test_add(a, b, expected):
assert add(a, b) == expected
这样一组逻辑可以跑多组数据。失败时 pytest 会指出是哪一组参数失败。面试里提到参数化,能体现你知道如何避免复制粘贴测试。
六、pytest 和 unittest 的关系
pytest 不是完全替代 unittest 的敌人。它可以运行大部分 unittest 风格测试,所以老项目可以逐步迁移。比如已有:
import unittest
class TestUser(unittest.TestCase):
def test_name(self):
self.assertEqual("Tom", "Tom")
pytest 也能发现并执行它。这对存量项目非常友好:团队可以保留旧测试,新测试采用 pytest 风格。
七、常见误区与追问
| 能力 | pytest 做法 | 面试要点 |
|---|---|---|
| 断言 | 直接写 assert | 失败时能展示表达式差异 |
| 资源准备 | fixture 注入 | 比到处写初始化更可复用 |
| 多组数据 | parametrize | 3 组输入可以复用 1 个测试函数 |
| 测试分组 | marker | 区分 slow、integration、smoke |
一次典型 pytest 执行:
发现 test_*.py -> 解析 fixture -> 执行参数化用例 -> 展示断言差异 -> 汇总失败位置
记忆钩子:pytest 的优势不是“少写几行”,而是让测试发现、资源复用、失败定位和工程扩展更顺手。
第一个误区是只会写 happy path。测试不只验证正常输入,也要覆盖边界、异常、空值、权限失败和外部依赖失败。
第二个误区是测试函数之间相互依赖。测试应该尽量独立,不能假设某个测试先运行,另一个测试再使用它留下的数据。
第三个误区是 fixture 滥用。fixture 太多、层层嵌套,会让测试读起来像谜语。fixture 应该帮助理解,而不是制造隐式魔法。
- 误区:pytest 只是 unittest 的简写版。 pytest 的核心价值还包括断言重写、fixture、参数化、marker 和插件生态,不只是语法更短。
- 误区:测试只覆盖正常路径就够了。 面试和工程都要关注边界、异常、空值、权限失败、外部依赖失败,否则测试很难挡住真实线上问题。
- 误区:fixture 越抽象越专业。 过深的 fixture 链会让测试数据来源不透明,测试允许有少量重复来换取可读性。
- 追问:pytest 如何兼容 unittest? pytest 可以发现并执行很多
unittest.TestCase风格的测试,老项目可以渐进迁移,新测试再采用 pytest 风格。 - 追问:参数化测试适合什么场景? 同一行为要验证多组输入输出时最适合,例如 3 个合法年龄、3 个非法金额、多个状态流转分支。
- 追问:marker 的工程价值是什么? marker 可以把测试分成
slow、integration、smoke等集合,让本地和 CI 按成本分层运行。
八、加强记忆
记 pytest 的核心价值:普通函数写测试,原生 assert 做断言,fixture 管资源,parametrize 管多组数据,插件补工程能力。它的优势不是单纯语法短,而是让测试更容易组织、复用、定位失败并融入工程流程。