【pytest】finalizer 执行顺序:FILO 原则

文章目录

核心概念:FILO(First-In-Last-Out,先进后出)

Finalizers 遵循 栈式执行顺序,类似于:

  • 函数调用栈
  • 资源获取即初始化(RAII)模式
  • 嵌套的 with 语句

原则:最后注册的 finalizer 最先执行


第一部分:Yield Fixtures 的执行顺序

代码示例

python 复制代码
def test_bar(fix_w_yield1, fix_w_yield2):
    print("test_bar")

@pytest.fixture
def fix_w_yield1():
    yield
    print("after_yield_1")

@pytest.fixture
def fix_w_yield2():
    yield
    print("after_yield_2")

执行结果

复制代码
test_bar
.after_yield_2
after_yield_1

详细执行流程分析

复制代码
时间轴 →

1. Setup 阶段(从左到右)
   ┌─────────────────┐
   │ fix_w_yield1    │ ← 第一个参数,先执行 setup
   │   执行到 yield   │
   └─────────────────┘
           ↓
   ┌─────────────────┐
   │ fix_w_yield2    │ ← 第二个参数,后执行 setup
   │   执行到 yield   │
   └─────────────────┘

2. 测试执行
   ┌─────────────────┐
   │   test_bar()    │ ← 打印 "test_bar"
   └─────────────────┘

3. Teardown 阶段(从右到左,栈式弹出)
   ┌─────────────────┐
   │ fix_w_yield2    │ ← 最后 setup 的,最先 teardown
   │ after_yield_2   │    打印 "after_yield_2"
   └─────────────────┘
           ↓
   ┌─────────────────┐
   │ fix_w_yield1    │ ← 最先 setup 的,最后 teardown
   │ after_yield_1   │    打印 "after_yield_1"
   └─────────────────┘

关键理解点

python 复制代码
def test_bar(fix_w_yield1, fix_w_yield2):
           #    ↑           ↑
           #   左边        右边(最后一个参数)
  • Setup 顺序 :左 → 右(fix_w_yield1fix_w_yield2
  • Teardown 顺序 :右 → 左(fix_w_yield2fix_w_yield1

为什么是这个顺序?

这是为了依赖关系的正确性

python 复制代码
@pytest.fixture
def database():
    db = create_db()
    yield db
    db.close()  # 最后关闭

@pytest.fixture
def user(database):  # 依赖 database
    u = database.create_user()
    yield u
    database.delete_user(u)  # 先删除用户

def test_something(database, user):
    pass

执行顺序:

  1. Setup: databaseuser
  2. Test 运行
  3. Teardown: userdatabase ✅(正确:先删用户,再关数据库)

如果反过来就会出错:

  • ❌ 先关数据库,再删用户 → 失败!

第二部分:addfinalizer 的执行顺序

代码示例

python 复制代码
from functools import partial
import pytest

@pytest.fixture
def fix_w_finalizers(request):
    request.addfinalizer(partial(print, "finalizer_2"))  # 第一个注册
    request.addfinalizer(partial(print, "finalizer_1"))  # 第二个注册

def test_bar(fix_w_finalizers):
    print("test_bar")

执行结果

复制代码
test_bar
.finalizer_1
finalizer_2

详细执行流程

复制代码
注册顺序(时间从上到下):
┌────────────────────────────┐
│ request.addfinalizer(f2)   │ ← 第一个注册(进栈)
├────────────────────────────┤
│ request.addfinalizer(f1)   │ ← 第二个注册(进栈)
└────────────────────────────┘

执行顺序(FILO,从栈顶弹出):
┌────────────────────────────┐
│ finalizer_1 执行           │ ← 最后注册的,最先执行
├────────────────────────────┤
│ finalizer_2 执行           │ ← 最先注册的,最后执行
└────────────────────────────┘

栈的可视化

复制代码
         栈结构

注册时:                执行时:
  
第二次注册              第一个执行
    ↓                      ↑
┌────────┐            ┌────────┐
│   f1   │  ← 栈顶    │   f1   │ ← 先出栈
├────────┤            ├────────┤
│   f2   │            │   f2   │ ← 后出栈
└────────┘            └────────┘
    ↑                      
第一次注册              第二个执行

实际应用示例

python 复制代码
@pytest.fixture
def complex_setup(request):
    # 步骤1:分配内存
    memory = allocate_memory()
    request.addfinalizer(lambda: free_memory(memory))
    
    # 步骤2:创建数据库连接(依赖内存)
    db = create_db_connection(memory)
    request.addfinalizer(lambda: db.close())
    
    # 步骤3:创建用户(依赖数据库)
    user = db.create_user()
    request.addfinalizer(lambda: db.delete_user(user))
    
    return user

清理顺序(FILO):

  1. ✅ 删除用户(最后注册,最先执行)
  2. ✅ 关闭数据库
  3. ✅ 释放内存(最先注册,最后执行)

这保证了依赖关系的正确性!


第三部分:底层实现揭秘

Yield Fixture 的底层实现原理

python 复制代码
# 我们写的代码:
@pytest.fixture
def my_fixture():
    print("setup")
    yield "resource"
    print("teardown")

# pytest 内部实际做的事(伪代码):
@pytest.fixture
def my_fixture(request):
    print("setup")
    
    # 创建生成器对象
    gen = generator_function()
    resource = next(gen)  # 执行到 yield,获取资源
    
    # 注册 finalizer
    def resume_generator():
        try:
            next(gen)  # 继续执行生成器,运行 yield 后的代码
        except StopIteration:
            pass
    
    request.addfinalizer(resume_generator)
    
    return resource

完整执行流程图

复制代码
┌─────────────────────────────────────────────────────┐
│  @pytest.fixture                                    │
│  def my_fixture():                                  │
│      print("setup")        ← 1. 执行到这里          │
│      yield "resource"      ← 2. 暂停,返回资源      │
│      print("teardown")     ← 4. finalizer 触发执行  │
└─────────────────────────────────────────────────────┘
                    ↓
            3. 测试运行完成
                    ↓
┌─────────────────────────────────────────────────────┐
│  request.addfinalizer(resume_generator)             │
│      ↓                                              │
│  resume_generator() 被调用                          │
│      ↓                                              │
│  next(gen) → 继续执行 yield 之后的代码              │
└─────────────────────────────────────────────────────┘

为什么要这样设计?

  1. 统一机制 :所有清理逻辑都通过 addfinalizer 管理
  2. 顺序保证:利用栈结构自动保证正确的清理顺序
  3. 异常安全:即使测试失败,finalizers 也会执行

多层嵌套示例

复杂场景

python 复制代码
@pytest.fixture
def fix_a():
    print("setup A")
    yield
    print("teardown A")

@pytest.fixture
def fix_b(fix_a):  # 依赖 fix_a
    print("setup B")
    yield
    print("teardown B")

@pytest.fixture
def fix_c(fix_b):  # 依赖 fix_b
    print("setup C")
    yield
    print("teardown C")

def test_example(fix_c):
    print("TEST")

执行结果

复制代码
setup A      ← 最底层依赖,先执行
setup B      ← 中间层
setup C      ← 最上层
TEST         ← 测试运行
teardown C   ← 最上层,先清理(FILO)
teardown B   ← 中间层
teardown A   ← 最底层,最后清理

调用栈可视化

复制代码
Setup(入栈):        Teardown(出栈):
                    
    ┌─────┐             ┌─────┐
    │  C  │ ← 最后      │  C  │ ← 最先
    ├─────┤             ├─────┤
    │  B  │             │  B  │
    ├─────┤             ├─────┤
    │  A  │ ← 最先      │  A  │ ← 最后
    └─────┘             └─────┘

实际应用建议

1. 利用 FILO 特性设计资源清理

python 复制代码
@pytest.fixture
def app_environment(request):
    # 按依赖顺序注册
    config = load_config()
    request.addfinalizer(lambda: config.cleanup())
    
    db = init_database(config)
    request.addfinalizer(lambda: db.shutdown())
    
    cache = init_cache(db)
    request.addfinalizer(lambda: cache.clear())
    
    return {'config': config, 'db': db, 'cache': cache}

清理顺序自动正确:cache → db → config ✅

2. 避免顺序陷阱

python 复制代码
# ❌ 错误:顺序会反过来
@pytest.fixture
def bad_example(request):
    request.addfinalizer(cleanup_step_1)  # 实际最后执行
    request.addfinalizer(cleanup_step_2)
    request.addfinalizer(cleanup_step_3)  # 实际最先执行
python 复制代码
# ✅ 正确:按想要的执行顺序反向注册
@pytest.fixture
def good_example(request):
    request.addfinalizer(cleanup_step_3)  # 想最后执行,先注册
    request.addfinalizer(cleanup_step_2)
    request.addfinalizer(cleanup_step_1)  # 想最先执行,最后注册

总结

方面 说明
核心原则 FILO(先进后出,栈式结构)
Yield fixtures 最右边的参数最先清理
addfinalizer 最后注册的最先执行
底层实现 yield 通过 addfinalizer 实现
设计目的 保证依赖资源的正确清理顺序
最佳实践 按依赖关系顺序创建资源,自动逆序清理

这种设计模式在编程中非常常见,就像俄罗斯套娃:最外层的最先关闭,最内层的最后关闭,确保不会出现"还在用的资源已经被销毁"的问题。

相关推荐
tao3556673 小时前
【Python刷力扣hot100】49. Group Anagrams
开发语言·python·leetcode
韩立学长3 小时前
【开题答辩实录分享】以《基于Python的新能源汽车管理系统的设计与实现》为例进行答辩实录分享
python·新能源汽车
云闲不收3 小时前
接口请求工具对比 apifox apipost swagger postman等
测试工具·postman
Pocker_Spades_A3 小时前
中秋与代码共舞:用Python、JS、Java打造你的专属中秋技术盛宴
python
梁萌4 小时前
自动化测试框架playwright使用
自动化测试·python·ui自动化·playwright
Python×CATIA工业智造4 小时前
Python回调函数中携带额外状态的完整指南:从基础到高级实践
python·pycharm
害恶细君4 小时前
【超详细】使用conda配置python的开发环境
开发语言·python·jupyter·pycharm·conda·ipython
java1234_小锋4 小时前
TensorFlow2 Python深度学习 - TensorFlow2框架入门 - 变量(Variable)的定义与操作
python·深度学习·tensorflow·tensorflow2
我星期八休息5 小时前
C++异常处理全面解析:从基础到应用
java·开发语言·c++·人工智能·python·架构