pytest 框架提供了 Fixture 夹具,使我们能够定义一个通用的设置步骤,该步骤可以反复使用,就像使用普通函数一样。不同的测试用例可以请求同一个 Fixture 进行同样的步骤处理,然后 pytest 会根据该 Fixture 为每个测试测试用例提供各自的结果数据。

这对于确保测试用例不会相互影响非常有用,我们可以使用此系统确保每个测试用例都获得自己的新数据,并从干净的状态开始,以便提供一致、可重复的结果。比如用户模块的各个测试用例,都可以被查询用户信息的 Fixture 夹具所装饰,或者运行每个测试用例前清理数据库数据。

定义 Fixture 函数


定义一个 Fixture 函数,非常简单,普通函数只需要被 @pytest.fixture 装饰即可。

import pytest

@pytest.fixture
def fixture_01():
    return True

使用 Fixture 函数


测试函数请求 Fixture 函数

import pytest

@pytest.fixture  
def fixture1():
    return True


def test_one(fixture1): 
    assert fixture1

def test_two(fixture1):
    assert fixture1

运行结果:

======================================================== test session starts ========================================================
platform linux -- Python 3.12.3, pytest-9.0.2, pluggy-1.6.0
configfile: pytest.toml
collected 2 items

cases/test_a.py ..                                                                                                            [100%]

========================================================= 2 passed in 0.01s =========================================================

测试函数请求多个 Fixture 函数

import pytest

@pytest.fixture
def fixture1():
    print("我是fixture1")

@pytest.fixture
def fixture2():
    print("我是fixture2")

@pytest.fixture
def fixture3():
    print("我是fixture3")


def test_one(fixture1, fixture2, fixture3):
    assert False

def test_two(fixture3, fixture2, fixture1):
    assert False

运行结果:

======================================================== test session starts ========================================================
platform linux -- Python 3.12.3, pytest-9.0.2, pluggy-1.6.0
configfile: pytest.toml
collected 2 items

cases/test_a.py FF                                                                                                            [100%]

============================================================= FAILURES ==============================================================
_____________________________________________________________ test_one ______________________________________________________________

fixture1 = None, fixture2 = None, fixture3 = None

    def test_one(fixture1, fixture2, fixture3): # fixture1 是
>       assert False
E       assert False

cases/test_a.py:17: AssertionError
------------------------------------------------------- Captured stdout setup -------------------------------------------------------
我是fixture1
我是fixture2
我是fixture3
_____________________________________________________________ test_two ______________________________________________________________

fixture3 = None, fixture2 = None, fixture1 = None

    def test_two(fixture3, fixture2, fixture1): # fixture1 是
>       assert False
E       assert False

cases/test_a.py:20: AssertionError
------------------------------------------------------- Captured stdout setup -------------------------------------------------------
我是fixture3
我是fixture2
我是fixture1
====================================================== short test summary info ======================================================
FAILED cases/test_a.py::test_one - assert False
FAILED cases/test_a.py::test_two - assert False
========================================================= 2 failed in 0.04s =========================================================

仔细看一下运行结果,会发现 Fixture 函数在测试用例中,是从左到右依次执行。test_one 和 test_two 参数中 Fixture 函数的位置不同,执行顺序也不同。请读者注意这点。

Fixture 函数请求 Fixture 函数

不过我不建议这样用,每个测试用例及其相关的函数都应该保持独立。

import pytest

@pytest.fixture
def fixture1():
    return 1

@pytest.fixture
def fixture2(fixture1):
    return fixture1 + 1

def test_one(fixture2): 
    assert fixture2 == 2

运行结果:

======================================================== test session starts ========================================================
platform linux -- Python 3.12.3, pytest-9.0.2, pluggy-1.6.0
configfile: pytest.toml
collected 1 item

cases/test_a.py .                                                                                                             [100%]

========================================================= 1 passed in 0.01s =========================================================

Fixture 函数的作用域


这里的作用域分为两个方面,一方面是可访问范围,另一方面是作用于测试用例的生命周期。

可访问范围


和普通函数一样,模块内定义的 Fixture 函数,其模块内所有测试用例都可以访问,可以参考上面的示例。

其他模块定义 Fixture 函数,需要导入访问

from fixture_fun import fixture1

def test_one(fixture1):
    assert fixture1

def test_two(fixture1):
    assert fixture1

模块文件 contest.py 内定义 Fixture 函数

pytest 框架提供一个特殊的模块文件 conftest.py,在其中定义的 Fixture 函数,不需要导入,测试用例可以直接访问。

其内 Fixture 函数的作用域,与 conftest.py 在项目中的位置有关。

接口自动化测试框架/
├── cases/
│   ├── conftest.py
│   ├── test_a.py
│   └── login/
│       ├── conftest.py
│       └── test_a.py
├── cases1/
└── conftest.py

上述目录结构场景中,根目录下 conftest.py 内定义的 Fixture 函数的作用域是整个项目,cases/ 目录下 conftest.py 内定义的 Fixture 函数的作用域是整个 cases/ ,login/ 目录下 conftest.py 内定义的 Fixture 函数的作用域是整个 login/ 。

如果不同 conftest.py 内定义了相同名称的 Fixture 函数,下级目录下的会覆盖上级目录下的。比如根目录下 conftest.py 和 login/ 目录下 conftest.py 内同时定义一个名为 fixture1 的 Fixture 函数,对于 login/ 目录下的测试用例来说,只会访问 login/ 目录下 conftest.py 内的 fixture1 夹具函数。

Fixture 函数的生命周期


Fixture 函数可以根据给定的 scope 值确定生命周期

  • function:(默认作用域)作用于单个测试函数 / 方法(测试用例)。每次测试用例请求该 Fixture 时,都会重新执行一遍 Fixture 函数。
  • class:作用于单个测试类。同一个测试类中所有测试用例请求该 Fixture 时,仅执行一遍;该类中其他请求该 Fixture 函数的测试用例获取第一次执行的缓存值。
  • module:作用于单个 Python 模块(.py 文件)。同一模块内所有测试用例请求该 Fixture 时,仅执行一遍;该模块中其他请求该 Fixture 函数的测试用例获取第一次执行的缓存值。
  • package:作用于定义 Fixture 的 Python 包(包含 init.py 的目录,及其子包、子目录)。整个包内所有测试用例请求该 Fixture 时,仅执行一遍;该 Python 包中其他请求该 Fixture 函数的测试用例获取第一次执行的缓存值。
  • session:作用于整个测试会话(session)周期。无论有多少测试用例、模块、包请求该 Fixture,仅执行一次;该 整个测试会话(session)周期中其他请求该 Fixture 函数的测试用例获取第一次执行的缓存值。

function 作用域

import pytest

@pytest.fixture()
def fixture1():
    print("我被执行了")

class Testcase():
    @staticmethod
    def test_one(fixture1): 
        assert False
    @staticmethod
    def test_two(fixture1): 
        assert False
    @staticmethod
    def test_three(fixture1): 
        assert False
        
def test_four(fixture1): 
    assert False
    
def test_five(fixture1): 
    assert False

运行结果:

======================================================== test session starts ========================================================
platform linux -- Python 3.12.3, pytest-9.0.2, pluggy-1.6.0
configfile: pytest.toml
collected 5 items

cases/test_a.py FFFFF                                                                                                         [100%]

============================================================= FAILURES ==============================================================
_________________________________________________________ Testcase.test_one _________________________________________________________

...
------------------------------------------------------- Captured stdout setup -------------------------------------------------------
我被执行了
_________________________________________________________ Testcase.test_two _________________________________________________________

...
------------------------------------------------------- Captured stdout setup -------------------------------------------------------
我被执行了
________________________________________________________ Testcase.test_three ________________________________________________________

...
------------------------------------------------------- Captured stdout setup -------------------------------------------------------
我被执行了
_____________________________________________________________ test_four _____________________________________________________________

...
------------------------------------------------------- Captured stdout setup -------------------------------------------------------
我被执行了
_____________________________________________________________ test_five _____________________________________________________________

...
------------------------------------------------------- Captured stdout setup -------------------------------------------------------
我被执行了
====================================================== short test summary info ======================================================
...
========================================================= 5 failed in 0.05s =========================================================

可以看到运行每个测试用例都执行一遍 fixture1,打印“我被执行了”。

class 作用域

import pytest

@pytest.fixture(scope="class")
def fixture1():
    print("我被执行了")

class Testcase():
    @staticmethod
    def test_one(fixture1): 
        assert False
    @staticmethod
    def test_two(fixture1): 
        assert False
    @staticmethod
    def test_three(fixture1): 
        assert False
        
def test_four(fixture1): 
    assert False
    
def test_five(fixture1): 
    assert False

运行结果:

======================================================== test session starts ========================================================
platform linux -- Python 3.12.3, pytest-9.0.2, pluggy-1.6.0
configfile: pytest.toml
collected 5 items

cases/test_a.py FFFFF                                                                                                         [100%]

============================================================= FAILURES ==============================================================
_________________________________________________________ Testcase.test_one _________________________________________________________

...
------------------------------------------------------- Captured stdout setup -------------------------------------------------------
我被执行了
_________________________________________________________ Testcase.test_two _________________________________________________________

...
________________________________________________________ Testcase.test_three ________________________________________________________

...
_____________________________________________________________ test_four _____________________________________________________________

...
------------------------------------------------------- Captured stdout setup -------------------------------------------------------
我被执行了
_____________________________________________________________ test_five _____________________________________________________________

...
------------------------------------------------------- Captured stdout setup -------------------------------------------------------
我被执行了
====================================================== short test summary info ======================================================
FAILED cases/test_a.py::Testcase::test_one - assert False
FAILED cases/test_a.py::Testcase::test_two - assert False
FAILED cases/test_a.py::Testcase::test_three - assert False
FAILED cases/test_a.py::test_four - assert False
FAILED cases/test_a.py::test_five - assert False
========================================================= 5 failed in 0.05s =========================================================

可以看到 Testcase 类中的测试用例,只有运行 Testcase::test_one 时执行一遍 fixture1,打印“我被执行了”。类外的测试用例运行会执行 fixture1。

module 作用域

import pytest

@pytest.fixture(scope="module")
def fixture1():
    print("我被执行了")

class Testcase():
    @staticmethod
    def test_one(fixture1): 
        assert False
    @staticmethod
    def test_two(fixture1): 
        assert False
    @staticmethod
    def test_three(fixture1): 
        assert False
        
def test_four(fixture1): 
    assert False
    
def test_five(fixture1): 
    assert False

运行结果:

======================================================== test session starts ========================================================
platform linux -- Python 3.12.3, pytest-9.0.2, pluggy-1.6.0
configfile: pytest.toml
collected 5 items

cases/test_a.py FFFFF                                                                                                         [100%]

============================================================= FAILURES ==============================================================
_________________________________________________________ Testcase.test_one _________________________________________________________

...
------------------------------------------------------- Captured stdout setup -------------------------------------------------------
我被执行了
_________________________________________________________ Testcase.test_two _________________________________________________________

...
________________________________________________________ Testcase.test_three ________________________________________________________

...r
_____________________________________________________________ test_four _____________________________________________________________

...
_____________________________________________________________ test_five _____________________________________________________________

...
====================================================== short test summary info ======================================================
...
========================================================= 5 failed in 0.04s =========================================================

可以看到该模块中的测试用例,只有第一个运行的 Testcase.test_one 被执行了一遍 fixture1。

其余的 package 和 session 作用域,不在演示,读者也应该十分清楚了。

自动使用 Fixture


定义 Fixture 函数时,参数 autouse 设置为 true,Pytest 会根据作用域自动决定测试用例请求该 Fixture 函数,常用于初始化操作。比如执行测试前,检查某个条件是否达到要求(如环境变量、配置文件、服务可达性等),如果不满足,直接退出。或者执行测试前,执行登录操作。

# conftest.py
import pytest
import os

@pytest.fixture(autouse=True, scope="session")
def check_test_condition():
    # 示例:检查环境变量是否设置
    env = os.getenv("TEST_ENV")
    if not env:
        pytest.exit("❌ 环境变量 TEST_ENV 未设置,终止所有测试!", returncode=1)

    if env not in ["dev", "staging"]:
        pytest.exit(f"❌ 不支持的环境: {env},仅支持 dev 或 staging", returncode=1)

使用 usefixtures 请求 Fixture 函数


当测试类或测试文件中的每个测试函数都需要请求某个具体 Fixture 函数时,可以使用 @pytest.mark.usefixtures 请求。

测试类中使用

import pytest

@pytest.fixture
def fixture_1():
    return True
    

@pytest.mark.usefixtures("fixture_1")
class TestDirectoryInit:
    def test_one(self):
        assert fixture_1

    def test_two(self):
        assert fixture_1

运行结果:

======================================================== test session starts ========================================================
platform linux -- Python 3.12.3, pytest-9.0.2, pluggy-1.6.0
configfile: pytest.toml
collected 2 items

cases/test_a.py ..                                                                                                            [100%]

========================================================= 2 passed in 0.01s =========================================================

测试文件中使用

import pytest

@pytest.fixture
def fixture_1():
    return True

pytestmark = pytest.mark.usefixtures("fixture_1")


class TestDirectoryInit:
    def test_one(self):
        assert fixture_1

    def test_two(self):
        assert fixture_1

def test_three():
    assert fixture_1

def test_four():
    assert fixture_1

运行结果:

======================================================== test session starts ========================================================
platform linux -- Python 3.12.3, pytest-9.0.2, pluggy-1.6.0
configfile: pytest.toml
collected 4 items

cases/test_a.py ....                                                                                                          [100%]

========================================================= 4 passed in 0.01s =========================================================

整个项目中使用

在根目录下的 conftest.py 中定义该 Fixture 函数(比如 fixture_1),然后在 pytest 配置文件中配置。运行时所有的测试用例都会请求 fixture_1 函数。

[pytest]
usefixtures = ["fixture_1"]

Fixture 函数作为工厂


这种用法在某些情况下特别好用。

import pytest


@pytest.fixture
def make_customer_record():
    def make_customer_record(name):
        return {"name": name, "orders": []}

    return make_customer_record


def test_customer_records(make_customer_record):
    customer_1 = make_customer_record("Lisa")
    assert customer_1["name"] == "Lisa"
    customer_2 = make_customer_record("Mike")
    assert customer_2["name"] == "Mike"
    customer_3 = make_customer_record("Meredith")
    assert customer_3["name"] == "Meredith"

运行结果:

======================================================== test session starts ========================================================
platform linux -- Python 3.12.3, pytest-9.0.2, pluggy-1.6.0
configfile: pytest.toml
collected 1 item

cases/test_a.py .                                                                                                             [100%]

========================================================= 1 passed in 0.01s =========================================================

在 Fixture 使用 yield


在 Fixture 函数中,还可以使用 yield 代替 return,yield 相比 return 多了执行 yield 后代码的功能。yield 后的代码可以称作该 Fixture 函数的拆卸代码。

yield 的逻辑

逻辑是这样的:运行测试用例时,首先执行 Fixture 函数;Fixture 函数内部执行到 yield 时,会将数据返回给测试用例;最后测试用例执行结束后,在运行拆卸代码。

我们先设想一个测试流程:

  1. 新建用户
  2. 查询数据库,是否新建成功
  3. 清理新建的用户

利用 yield,我们可以这样写:

import pytest

# 使用字典模拟数据库
user = {}

@pytest.fixture()
def add_user():
    user['user1'] = 'JZY'

@pytest.fixture()
def select_user():
    yield user['user1']
    user.pop('user1')

def test_one(add_user, select_user):
    assert select_user == 'JZY', "测试失败"

def test_two():
    assert user == {'user1': 'JZY'}, "测试失败"

运行结果:

======================================================== test session starts ========================================================
platform linux -- Python 3.12.3, pytest-9.0.2, pluggy-1.6.0
configfile: pytest.toml
collected 2 items

cases/test_a.py .F                                                                                                            [100%]

============================================================= FAILURES ==============================================================
_____________________________________________________________ test_two ______________________________________________________________

    def test_two():
>       assert user == {'user1': 'JZY'}, "测试失败"
E       AssertionError: 测试失败
E       assert {} == {'user1': 'JZY'}
E
E         Right contains 1 more item:
E         {'user1': 'JZY'}
E         Use -v to get more diff

cases/test_a.py:19: AssertionError
====================================================== short test summary info ======================================================
FAILED cases/test_a.py::test_two - AssertionError: 测试失败
==================================================== 1 failed, 1 passed in 0.04s ====================================================

拆卸代码的执行顺序

上面已经说过,如果一个测试用例请求多个 Fixture 函数,Fixture 函数从左到右依次执行。但是 Fixture 函数的拆卸代码的执行顺序是从右到左。

import pytest

@pytest.fixture
def fix_w_yield1():
    print("我是fix_w_yield1")
    yield
    print("我是fix_w_yield1的拆卸代码")


@pytest.fixture
def fix_w_yield2():
    print("我是fix_w_yield2")
    yield
    print("我是fix_w_yield2的拆卸代码")

def test_bar(fix_w_yield1, fix_w_yield2):
    assert False

运行结果:

======================================================== test session starts ========================================================
platform linux -- Python 3.12.3, pytest-9.0.2, pluggy-1.6.0
configfile: pytest.toml
collected 1 item

cases/test_a.py F                                                                                                             [100%]

============================================================= FAILURES ==============================================================
_____________________________________________________________ test_bar ______________________________________________________________

...
------------------------------------------------------- Captured stdout setup -------------------------------------------------------
我是fix_w_yield1
我是fix_w_yield2
----------------------------------------------------- Captured stdout teardown ------------------------------------------------------
我是fix_w_yield2的拆卸代码
我是fix_w_yield1的拆卸代码
====================================================== short test summary info ======================================================
...
========================================================= 1 failed in 0.04s =========================================================

可以看到,先执行 fix_w_yield2 的拆卸代码,后执行 fix_w_yield1 的拆卸代码。


我在本文中介绍的只是我会使用的一些用法,还有一些 Fixtures 的用法没有在文章中说明,感兴趣的读者可以访问 How to use fixtures 了解,里面有很详细的介绍。


THEEND



© 转载需要保留原始链接,未经明确许可,禁止商业使用。CC BY-NC-ND 4.0