pytest解读fixtures之Teardown处理yield和addfinalizer方案

宋时安
宋时安
发布时间:2026-10-15| 5人浏览过
原创

本文深入解析pytest中fixtures的teardown机制,详细对比yield和addfinalizer两种实现方式。通过具体代码示例展示yield fixtures的执行顺序及addfinalizer注册多个终结器的用法,并探讨两者在异常处理上的差异,帮助开发者编写更健壮的测试清理代码。

前言

在运行测试函数时,确保测试结束后能自动清理环境产生的影响至关重要。这样可以避免对其他测试函数造成干扰,防止测试数据随时间推移而不断累积。

熟悉unittest框架的开发者应该了解teardown函数的作用,其功能与清理环境类似。在此,我们将这类负责“善后”工作的代码统称为teardown代码。

pytest框架中的fixture机制同样提供了强大的支持,允许我们在其中定义teardown代码。

实现这一功能主要有两种方法,分别是yield和addfinalizer。

一、yield fixtures(推荐)

1, yield 和 return

在包含yield的fixtures函数中,关键字yield 可以替代 return,用于将fixture中的对象传递给调用该fixture的函数或测试函数。

其用法与其他普通fixture函数相似,主要区别在于:

  • yield替换掉了return
  • teardown代码放置在yield之后

2, yield的执行顺序

pytest执行fixture函数时,会依据fixture之间的线性依赖关系按顺序调用。然而,当测试函数执行完毕后,pytest会按照之前的调用顺序反向执行yield之后的代码。

结合以下示例进行观察,此处未引用官方示例,而是手写一个更直观的案例:

import pytest
@pytest.fixture
def fixture_one():
    print("\n执行fixture_one")
    return 1
@pytest.fixture
def fixture_two(fixture_one):
    print("\n执行fixture_two")
    yield 2
    print("\n执行fixture_two的teardown代码")
@pytest.fixture
def fixture_adding(fixture_one, fixture_two):
    print("\n执行fixture_adding")
    result = fixture_one + fixture_two
    yield result
    print("\n执行fixture_adding的teardown代码")
def test_demo(fixture_two, fixture_adding):
    print("\n执行测试函数test_demo")
    assert fixture_adding == 3

代码中,fixture函数调用了其他fixture,测试函数也调用了多个fixture。通过前几章的学习,相信读者已经能够梳理出前后的调用顺序:

  • test_demo 测试函数首先调用fixture函数 fixture_two,随后调用 fixture_adding。
  • 在fixture函数 fixture_two中,进一步调用了另一个fixture函数 fixture_one。
  • 在fixture函数 fixture_adding中,调用了 fixture_one和 fixture_two。

因此,fixture函数的初始化顺序为:fixture_one、fixture_two、fixture_adding。据此可推断,测试结束后的teardown代码执行顺序为:fixture_adding、fixture_two。

运行代码以验证结果是否符合上述梳理:

============================= test session starts =============================
platform win32 -- Python 3.6.8, pytest-5.4.3, py-1.9.0, pluggy-0.13.1
rootdir: D:\练习\demo_fixture
plugins: allure-pytest-2.8.32, celery-4.3.0, Faker-4.14.2, base-url-1.4.2, html-2.1.1, metadata-1.10.0collected 1 item
test_module.py 
执行fixture_one
执行fixture_two
执行fixture_adding
.
执行测试函数test_demo
执行fixture_adding的teardown代码
执行fixture_two的teardown代码
                                                         [100%]
============================== 1 passed in 0.09s ==============================

运行结果与之前的分析完全一致。

需要注意的是,即使teardown代码按正确顺序执行,也不能保证所有代码都能正常执行。例如,若teardown中的某段代码发生异常,可能导致后续的清理动作无法执行。这引出了另一个关键点:健壮的fixture结构应具备何种特性。官方文档对此有单独说明,此处亦不作展开。

二、addfinalizer

1.request.addfinalizer把函数变成终结器

在pytest中进行teardown处理,除了使用带有yield的fixture函数外,还可以直接添加终结器。直接参考示例代码:

import pytest
@pytest.fixture()
def demo_fixture(request):
    print("\n这个fixture在每个case前执行一次")
    def demo_finalizer():
        print("\n在每个case完成后执行的teardown")
    #注册demo_finalizer为终结函数    
    request.addfinalizer(demo_finalizer)
def test_01(demo_fixture):
    print("\n===执行了case: test_01===")
def test_02(demo_fixture):
    print("\n===执行了case: test_02===")

查看运行结果:

============================= test session starts =============================
platform win32 -- Python 3.6.8, pytest-5.4.3, py-1.9.0, pluggy-0.13.1
rootdir: D:\练习\demo_fixture
plugins: allure-pytest-2.8.32, celery-4.3.0, Faker-4.14.2, base-url-1.4.2, html-2.1.1, metadata-1.10.0collected 2 items
test_module.py 
这个fixture在每个case前执行一次
.
===执行了case: test_01===
在每个case完成后执行的teardown
这个fixture在每个case前执行一次
.
===执行了case: test_02===
在每个case完成后执行的teardown
                                                        [100%]
============================== 2 passed in 0.10s ==============================
Process finished with exit code 0

从运行结果可以看出,其效果与yield一致。这是一种固定写法,关于request的文档中另有讲解,届时再行分享。

2.request.addfinalizer注册多个终结器函数

上述代码仅展示了一个终结函数,若要注册多个终结器该如何操作?

import pytest
@pytest.fixture()
def demo_fixture(request):
    print("\n这个fixture在每个case前执行一次")
    def demo_finalizer():
        print("\n在每个case完成后执行的teardown")
    def demo_finalizer2():
        print("\n在每个case完成后执行的teardown2")
    #注册demo_finalizer为终结函数
    request.addfinalizer(demo_finalizer)
    request.addfinalizer(demo_finalizer2)
def test_01(demo_fixture):
    print("\n===执行了case: test_01===")
def test_02(demo_fixture):
    print("\n===执行了case: test_02===")
if __name__ == '__main__':
    pytest.main(['-s', 'test_module.py'])

运行结果:

============================= test session starts =============================
platform win32 -- Python 3.6.8, pytest-5.4.3, py-1.9.0, pluggy-0.13.1
rootdir: D:\练习\demo_fixture
plugins: allure-pytest-2.8.32, celery-4.3.0, Faker-4.14.2, base-url-1.4.2, html-2.1.1, metadata-1.10.0collected 2 items
test_module.py 
这个fixture在每个case前执行一次
.
===执行了case: test_01===
在每个case完成后执行的teardown2
在每个case完成后执行的teardown
这个fixture在每个case前执行一次
.
===执行了case: test_02===
在每个case完成后执行的teardown2
在每个case完成后执行的teardown
                                                        [100%]
============================== 2 passed in 0.09s ==============================
Process finished with exit code 0

这里需要特别注意,在注册多个终结器的情况下,其执行顺序与注册时的顺序相反。

3.yield和addfinalizer的区别

目前从官方文档中了解到:

We have to be careful though, because pytest will run that finalizer once it’s been added, even if that fixture raises an exception after adding the finalizer.

一旦添加了终结器,pytest便会执行它。

然而,当我尝试在setup代码中进行抛错时,终结器的代码并未执行。

尝试搜索外网暂时未得到有效帮助,只能在GitHub上向pytest提交issue。此处暂留一个待解决的坑,更多关于Teardown处理yield和addfinalizer的资料请关注本站其它相关文章!

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。