当前位置:首页 > 文章列表 > 文章 > python教程 > pytest Fixture 作用域如何影响测试隔离与速度

pytest Fixture 作用域如何影响测试隔离与速度

来源:17golang原创 2026-10-08 15:38:52 0浏览 收藏

在测试数量还少的时候,Fixture 用默认配置通常没什么问题。等到用例增长、数据库或容器初始化越来越重,团队很容易走向两个极端:要么每个测试都重新创建昂贵资源,套件慢得难以接受;要么把所有 Fixture 都改成 session,速度是快了,却开始出现单测独立运行能过、整套运行偶发失败的问题。

先给结论:Fixture 的 scope 本质上定义了“一个实例被缓存并共享到多大范围”。作用域越小,隔离通常越强,初始化和清理次数也越多;作用域越大,重复初始化越少,但共享可变状态会放大测试耦合。正确做法不是一味扩大 scope,而是把“昂贵且稳定的基础设施”和“每个测试都会修改的业务状态”拆成两层。

pytest Fixture 官方文档:https://docs.pytest.org/en/stable/how-to/fixtures.html

先看结论:scope 决定缓存边界

pytest 提供五种常用作用域:function、class、module、package 和 session。默认值是 function。它们的区别不是 Fixture 能否被找到,而是同一个 Fixture 实例在多久之后被销毁和重新创建。

作用域大致生命周期隔离性典型用途
function每个测试函数一次最强可变对象、事务、临时目录、Mock
class每个测试类一次较强类内只读上下文、成组场景
module每个测试模块一次中等模块级静态样本、只读客户端
package每个测试包一次较弱包级服务或数据集
session整次 pytest 运行一次最弱容器、连接池、只读配置、昂贵服务

这里的“隔离性最弱”不等于 session scope 一定危险。真正危险的是把会被测试修改、又没有恢复机制的对象放进大作用域。一个只读配置对象放在 session 级通常很安全;一个所有测试共同增删记录的列表放在 session 级则很容易污染后续断言。

pytest Fixture 五种作用域与缓存边界示意图
Fixture 作用域越大,实例复用边界越宽;越接近 function,测试之间的天然隔离越细。

用户任务拆解:你真正想复用的是什么

我在调整大型测试套件时,会先把资源分成三类,而不是直接问“这个 Fixture 要不要改成 session”。

  1. 创建很便宜、状态会变化:优先使用 function。例如列表、请求上下文、临时文件、Mock 对象。
  2. 创建很昂贵、运行时基本只读:可以考虑 module、package 或 session。例如 Docker 服务、数据库引擎、模型权重、只读测试语料。
  3. 创建很昂贵、测试又必须修改:拆成两层。外层复用基础设施,内层为每个测试创建事务、命名空间或可回滚状态。

第三类最值得关注。很多“隔离和速度只能二选一”的困境,其实是因为 Fixture 把连接池、数据库连接、事务和业务数据揉在了一个对象里。拆开以后,连接池可以整场复用,而事务仍然每个测试独立。

组件实现一:可变状态默认使用 function

下面的购物车 Fixture 没写 scope,因此默认是 function。两个测试拿到的是两个不同的列表,前一个测试追加的数据不会泄漏给后一个测试。

import pytest


@pytest.fixture
def cart():
    # 每个测试都获得独立列表,避免跨测试污染
    return []


def test_add_product(cart):
    # 本测试只修改自己的购物车
    cart.append("keyboard")
    assert cart == ["keyboard"]


def test_cart_starts_empty(cart):
    # 即使前一个测试添加了商品,这里仍是空列表
    assert cart == []

这种方式的价值不只是避免脏数据。它还让测试顺序不重要:单独运行、随机排序运行或并行运行时,行为更接近。只要对象创建成本很低,就没有必要为了少执行几次构造函数而牺牲这个特性。

组件实现二:session 引擎加 function 事务

数据库测试是最典型的双层结构。创建数据库引擎和连接池可能很慢,但“本测试写入的数据”必须在测试结束后撤销。可以让引擎使用 session scope,让事务和 ORM Session 保留默认 function scope。

import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import Session


@pytest.fixture(scope="session")
def db_engine():
    # 昂贵的引擎与连接池在整次测试会话中只创建一次
    engine = create_engine("postgresql+psycopg://tester:secret@127.0.0.1/app_test")
    yield engine
    # 所有测试结束后统一释放底层连接池
    engine.dispose()


@pytest.fixture
def db_session(db_engine):
    # 每个测试打开独立连接和事务,保留函数级隔离
    connection = db_engine.connect()
    transaction = connection.begin()
    session = Session(bind=connection)

    yield session

    # 即使测试失败,yield 之后的清理仍会执行
    session.close()
    transaction.rollback()
    connection.close()

这段结构里,速度来自 session 级的引擎复用,隔离来自 function 级事务的回滚。测试不应该主动提交到外层事务之外;如果业务代码必须测试真实提交,可以使用嵌套事务、独立 schema 或每测试唯一数据库名,原则仍然是“共享基础设施,不共享未清理的业务状态”。

pytest 会话级基础设施与函数级隔离层组合图
上层引擎与连接池只初始化一次,下层事务按测试创建并回滚,兼顾启动成本和状态隔离。

可见性与依赖:conftest.py 怎么放

Fixture 的“定义位置”与“生命周期”是两个不同问题。放在某个目录的 conftest.py 中,会让该目录及其下级目录的测试可以发现它;但它究竟每个函数创建一次还是整场创建一次,仍由 scope 决定。

建议把 Fixture 放在最小的合理可见范围内:

  • 只被一个模块使用的 Fixture,直接放在该测试模块中。
  • 被一个业务目录复用的 Fixture,放在该目录的 conftest.py。
  • 真正跨项目通用的基础设施,才放在测试根目录的 conftest.py。

pytest 安排 Fixture 时会考虑作用域、依赖关系和 autouse,而不是按函数在文件中的书写顺序。高作用域 Fixture 在同一请求链中通常会先于低作用域 Fixture 准备。与其依赖“看起来在上面”,不如把依赖写进参数:

import pytest


@pytest.fixture(scope="session")
def api_server():
    # 整场复用已经启动的测试服务
    server = start_test_server()
    yield server
    # 会话结束后关闭服务
    server.stop()


@pytest.fixture
def authenticated_client(api_server):
    # 参数依赖明确表达:先有服务,再创建本测试客户端
    client = make_client(api_server)
    client.login_as("test-user")
    return client

性能检查:先测量,再扩大作用域

看到测试慢,不要先把所有 Fixture 都改成 session。慢点可能在业务代码、网络超时、数据工厂或重复迁移。pytest 自带耗时报告,可以先列出最慢用例:

# 列出最慢的 10 个测试,并显示超过 0.05 秒的阶段
pytest --durations=10 --durations-min=0.05

如果确认慢在 Fixture 初始化,可以在创建处加计数日志,或用 pytest 的 setup/teardown 输出观察调用频率。一次只调整一个资源,然后比较总耗时、最慢用例和失败稳定性。速度提升如果伴随随机失败,往往说明共享状态没有被正确清理。

还要注意一个容易误判的细节:pytest 会缓存当前作用域中的 Fixture 实例,但参数化 Fixture 在同一作用域内仍可能因不同参数而创建多次。因此,“session scope”不能被简单理解为“整个运行永远只调用一次”。

边界状态:参数化、动态作用域与清理

1. 参数化会形成多组缓存实例

import pytest


@pytest.fixture(scope="session", params=["sqlite", "postgresql"])
def backend(request):
    # 每个参数值都需要自己的后端实例
    instance = start_backend(request.param)
    yield instance
    # 对应参数实例使用结束后执行清理
    instance.stop()

这类 Fixture 虽是 session scope,但测试会覆盖两种后端,因此不能假设只启动一个实例。估算性能时应把参数数量也计算进去。

2. 动态作用域适合显式运行模式

有时本地开发希望每个测试都重建容器,而 CI 希望整场复用。pytest 支持通过可调用对象动态返回 scope。这个可调用对象会在 Fixture 定义时执行一次,适合由命令行选项决定生命周期。

import pytest


def determine_scope(fixture_name, config):
    # 显式传入 --keep-container 时整场复用,否则保持函数级隔离
    return "session" if config.getoption("--keep-container") else "function"


@pytest.fixture(scope=determine_scope)
def service_container():
    # scope 由运行参数决定,资源创建逻辑保持一致
    container = start_container()
    yield container
    # 当前作用域结束后释放容器
    container.stop()

动态 scope 不应变成隐藏的环境猜测。最好通过明确的命令行选项控制,并在 CI 配置中固定下来,否则开发机与流水线可能得到完全不同的隔离行为。

3. yield 清理必须与资源创建成对

作用域越大,清理遗漏的影响越大。推荐把创建和释放写在同一个 Fixture 的 yield 前后。若创建过程包含多个可能失败的步骤,应在每一步成功后登记对应清理,避免中途异常留下端口、进程或连接。

一张决策表选对 Fixture 作用域

资源特征推荐策略原因
便宜且可变function隔离收益远大于重复创建成本
昂贵且只读module / session可以安全减少初始化次数
昂贵且可回滚大 scope 基础设施 + function 隔离层同时获得复用和独立状态
只服务一个测试类class边界与使用范围一致
跨目录共享但不是全局package 或局部 conftest避免把可见性和生命周期放大到全项目
无法可靠清理的外部状态优先 function 或唯一命名空间不要用共享换取表面速度

最后可以记住一句话:scope 应该匹配资源生命周期,而不是匹配你希望测试有多快。先用 function 获得可信的隔离基线,再把确认昂贵、可安全复用的基础设施逐层提升到 module、package 或 session;任何共享可变状态,都必须有事务、重置、唯一命名空间或可靠清理作为补偿。这样优化后的测试套件,既快,也不会把偶发失败留给下一位维护者。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
html/template 与 text/template 的转义边界有什么不同html/template 与 text/template 的转义边界有什么不同
上一篇
html/template 与 text/template 的转义边界有什么不同
把模板函数注册、解析与执行错误分别处理
下一篇
把模板函数注册、解析与执行错误分别处理
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    378次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    449次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    457次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    400次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    227次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码