用 pytest fixture 组织可维护的测试:告别一堆重复的 setUp

用 pytest fixture 组织可维护的测试:告别一堆重复的 setUp

写测试写到后面,你八成会遇到这个场景:二十个测试函数,每个开头都要先建一个数据库连接、塞几条测试数据、跑完再清掉。复制粘贴到第五个函数你就烦了,而且哪天连接方式变了,二十处都得改。pytest 的 fixture 就是专门治这个的------它把「准备环境」和「测试逻辑」彻底分开,而且比 unittest 的 setUp/tearDown 灵活得多。这篇把 fixture 从基础用到进阶讲透。

朴素写法的痛:每个测试都在重复搭台子

假设我们测一个简单的用户仓储类。不用 fixture 时,代码大概长这样:

python 复制代码
def test_add_user():
    db = Database(":memory:")  # 每个测试都自己建库
    db.init_schema()
    repo = UserRepo(db)

    repo.add(User(name="Alice"))
    assert repo.count() == 1

    db.close()  # 每个测试都自己收尾

def test_find_user():
    db = Database(":memory:")  # 又抄一遍
    db.init_schema()
    repo = UserRepo(db)

    repo.add(User(name="Bob"))
    found = repo.find_by_name("Bob")
    assert found.name == "Bob"

    db.close()

问题一目了然:建库那三行、关库那一行,每个测试都在重复。真正的测试逻辑只有中间两三行,却被样板淹没。而且 db.close() 一旦某个测试中途 assert 失败就不会执行,资源泄漏。

fixture 基础:把「搭台子」抽出来

fixture 就是一个用 @pytest.fixture 装饰的函数,它准备好资源,通过 yield 交给测试用,yield 之后的代码就是清理逻辑(相当于 tearDown,而且 assert 失败也照样执行):

python 复制代码
import pytest

@pytest.fixture
def repo():
    db = Database(":memory:")
    db.init_schema()
    yield UserRepo(db)  # yield 出去的东西就是测试拿到的值
    db.close()          # 测试结束后(哪怕失败)一定执行清理

测试函数只要把 fixture 的名字写进参数,pytest 会自动注入:

python 复制代码
def test_add_user(repo):  # 参数名 == fixture 名,自动注入
    repo.add(User(name="Alice"))
    assert repo.count() == 1

def test_find_user(repo):
    repo.add(User(name="Bob"))
    assert repo.find_by_name("Bob").name == "Bob"

现在每个测试只剩纯逻辑。更关键的是:每个测试函数拿到的都是一个全新的 repotest_add_user 里加的 Alice 不会污染 test_find_user------因为默认每个测试都会重新执行一遍 fixture。测试之间互相隔离,这是可维护测试的地基。

scope:控制 fixture 多久重建一次

「每个测试都重建」是默认行为(scope="function"),隔离性最好但最慢。如果你的 fixture 很重(比如启动一个真实数据库容器、加载一个大模型),每个测试都重来一遍会慢到没法忍。这时用 scope 调节:

python 复制代码
@pytest.fixture(scope="session")  # 整个测试会话只建一次
def db_engine():
    engine = create_engine("postgresql://localhost/test")
    yield engine
    engine.dispose()

scope 可选值,从窄到宽:

  • function(默认):每个测试函数重建一次,隔离最好。
  • class:每个测试类重建一次。
  • module:每个 .py 文件重建一次。
  • session:整个 pytest 运行只建一次。

实战里常见组合 :重的、只读的资源用 session(比如数据库引擎、连接池);会被测试改动的、需要隔离的资源用 function。比如引擎用 session 建一次,但每个测试开一个会回滚的事务来保证隔离:

python 复制代码
@pytest.fixture(scope="session")
def db_engine():
    engine = create_engine("postgresql://localhost/test")
    yield engine
    engine.dispose()

@pytest.fixture  # function 级,每个测试一个干净事务
def db_session(db_engine):  # fixture 可以依赖另一个 fixture
    conn = db_engine.connect()
    trans = conn.begin()
    session = Session(bind=conn)
    yield session
    session.close()
    trans.rollback()  # 回滚,测试写入的数据不落库,天然隔离
    conn.close()

注意 db_sessiondb_engine 当参数------fixture 之间可以互相依赖,pytest 会自动按依赖顺序构建。这是 fixture 比 setUp 强大的地方:setUp 只能是一整块,fixture 可以像积木一样组合。

conftest.py:让 fixture 跨文件共享

fixture 写在测试文件里,只能本文件用。当多个测试文件都要用同一个 db_session,把它挪到 conftest.py------这是 pytest 的约定文件,放在测试目录下,同目录及子目录的所有测试自动能用里面的 fixture,不需要 import:

复制代码
tests/
├── conftest.py         # 放公共 fixture:db_engine, db_session
├── test_users.py       # 直接用 db_session,不用 import
└── api/
    ├── conftest.py     # 放 api 专用 fixture:client
    └── test_orders.py  # 能用上层的 db_session + 本层的 client
python 复制代码
# tests/conftest.py
import pytest

@pytest.fixture
def db_session(db_engine):
    ...  # 同上

子目录的 conftest.py 可以有自己的 fixture,也能覆盖上层同名 fixture。这种分层让公共的放上面、专用的放下面,结构清晰。

进阶:参数化 fixture,一份逻辑测多种输入

有时你想用多组数据 跑同一个测试。fixture 的 params 能做到------pytest 会为每个参数值把测试跑一遍:

python 复制代码
@pytest.fixture(params=["sqlite", "postgres"])
def repo(request):  # request 是内置对象,request.param 拿到当前参数
    if request.param == "sqlite":
        db = Database(":memory:")
    else:
        db = Database("postgresql://localhost/test")
    db.init_schema()
    yield UserRepo(db)
    db.close()

def test_add_user(repo):
    repo.add(User(name="Alice"))
    assert repo.count() == 1

这一个 test_add_user 会自动跑两次:一次用 SQLite、一次用 Postgres。写一份测试逻辑,覆盖多种后端。

一个常见误区:别用 return 代替 yield 还想清理

新手常犯的错:fixture 里用 return 而不是 yield

python 复制代码
# 反例:return 之后没法写清理逻辑
@pytest.fixture
def repo():
    db = Database(":memory:")
    db.init_schema()
    return UserRepo(db)  # 用 return 也能注入,但 db 永远不会 close

return 版本能用,但 return 之后的代码在 fixture 里不会有清理时机------数据库连接泄漏。只要资源需要清理(关连接、删临时文件、停容器),就用 yield ,把清理写在 yield 之后。不需要清理的纯数据 fixture(比如返回一个固定字典)用 return 才合适。

小结

  • fixture 把「准备环境」从测试逻辑里剥离,用 @pytest.fixture + 参数注入,测试函数只留纯逻辑。
  • yield 前是准备、后是清理,且清理在测试失败时也会执行,比 setUp/tearDown 更可靠;纯数据用 return,需清理用 yield
  • 默认 function scope 保证测试间隔离;重资源用 session 建一次 + function 级事务回滚兼顾速度与隔离。
  • fixture 可以互相依赖、像积木一样组合;公共 fixture 放 conftest.py,同目录及子目录免 import 直接用。
  • params 让一份测试逻辑跑多组输入,配合 request.param 覆盖多种后端。
  • 一句话记忆点:fixture 管「测试需要什么环境」,测试函数只管「验证什么行为」,两者一分开,测试就好维护了。
相关推荐
明志数科2 小时前
人形机器人格斗场景下的技术挑战:动力学控制、感知融合与实时决策
网络·数据库
酉鬼女又兒2 小时前
零基础入门 DeepSeek V4 Pro API 开发:从环境搭建、消息格式规范到翻译函数实战、少样本提示、多轮对话聊天机器人与常见报错全流程详解指南
大数据·网络·数据库·人工智能·macos·机器人·github
笨蛋不要掉眼泪2 小时前
MySQL架构揭秘:MVCC解决读一致性问题
数据库·mysql·架构
Navicat中国3 小时前
开发者入门:无服务器数据库
数据库·云原生·serverless·navicat
梅雅达编程笔记4 小时前
编程启蒙|Scratch 转 Python 系列第9天:字典/哈希表积木双向对照(AI大模型参数配置表实战)
开发语言·人工智能·python·numpy·pandas
whn19775 小时前
如何避免以root用户执行达梦
数据库
zhz52145 小时前
GIS项目中空间参考转换与MBTiles偏移:问题成因、解法与避坑
python·vue·gis
brave_zhao5 小时前
openEuler下安装tar包解压工具
linux·服务器·数据库
KaMeidebaby6 小时前
卡梅德生物技术快报|如何制备单克隆抗体:小众禽类靶点单抗制备实操流程:双载体抗原交叉筛选完整工艺记录
人工智能·python·深度学习·算法·机器学习
乱写代码6 小时前
Pydantic学习--BaseModel
python