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

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

PS: 本文基于pytest 8.3.3

简单使用 Fixture

测试用例使用 Fixture

# \Test\test_mod1.py
import pytest

@pytest.fixture
def fixture_01():
    return [1, 2, 3]

def test_01(fixture_01):
    assert fixture_01 == [1, 2, 3], "测试失败"


def test_02(fixture_01):
    assert fixture_01 == [1, 2], "测试失败"

运行结果:

===================================================================== test session starts ======================================================================
configfile: pytest.ini
plugins: dependency-0.6.0, order-1.3.0
collected 4 items                                                                                                                                                

Config::test_01 PASSED                                                                                                                                    [ 25%] 
Config::test_02 FAILED                                                                                                                                    [ 50%]
Config::TestClass::test_03 PASSED                                                                                                                         [ 75%] 
Config::TestClass::test_04 FAILED                                                                                                                         [100%] 

=================================================================== short test summary info ==================================================================== 
FAILED Config::test_02 - AssertionError: 测试失败
FAILED Config::TestClass::test_04 - AssertionError: 测试失败
================================================================= 2 failed, 2 passed in 0.03s ================================================================== 

运行测试用例之前,先执行 Fixture 函数,并将返回值作为参数传给测试用例。

测试用例使用多个 Fixture

# \Test\test_mod1.py
import pytest

@pytest.fixture
def fixture_01():
    return [1, 2, 3]

@pytest.fixture
def fixture_02():
    return [1, 2, 3]

def test_01(fixture_01, fixture_02):
    assert fixture_01 == fixture_02, "测试失败"

运行结果:

===================================================================== test session starts ======================================================================
configfile: pytest.ini
plugins: dependency-0.6.0, order-1.3.0
collected 1 item                                                                                                                                                 

Config::test_01 PASSED                                                                                                                                    [100%] 

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

Fixture 函数可以被多个测试用例复用。

但是要注意执行顺序是从左到右:

# \Test\test_mod1.py
import pytest

data = {"a": 2, "b": 2}

# data["a"]
@pytest.fixture
def fixture_a_01():
    data["a"] *= 10


@pytest.fixture
def fixture_a_02():
    data["a"] += 10

def test_a(fixture_a_01, fixture_a_02):
    assert data["a"] == 30, "测试失败"

# data["b"]
@pytest.fixture
def fixture_b_01():
    data["b"] *= 10


@pytest.fixture
def fixture_b_02():
    data["b"] += 10

def test_b(fixture_b_02, fixture_b_01):
    assert data["b"] == 120, "测试失败"

运行结果:

===================================================================== test session starts ======================================================================
configfile: pytest.ini
plugins: dependency-0.6.0, order-1.3.0
collected 2 items                                                                                                                                               

Config::test_a PASSED                                                                                                                                     [ 50%] 
Config::test_b PASSED                                                                                                                                     [100%] 

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

执行顺序不一样,往往会导致结果不同。

Fixture 使用其它 Fixture

# \Test\test_mod1.py
import pytest

@pytest.fixture
def fixture_01():
    return [1, 2, 3]

@pytest.fixture
def fixture_02(fixture_01):
    return fixture_01

def test_01(fixture_02):
    assert fixture_02 == [1, 2, 3], "测试失败"

运行结果:

===================================================================== test session starts ======================================================================
configfile: pytest.ini
plugins: dependency-0.6.0, order-1.3.0
collected 1 item                                                                                                                                                 

Config::test_01 PASSED                                                                                                                                    [100%] 

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

Fixture 函数也可以使用其它 Fixture 函数。

自动使用 Fixture

# \Test\test_mod1.py
import pytest


a = []
@pytest.fixture(autouse=True)
def fixture_01():
    a.append(1)



def test_01():
    assert a == [1], "测试失败"

不是很推荐使用,不好管理。

Fixture 函数作为工厂

# \Test\test_mod1.py
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 ======================================================================
configfile: pytest.ini
plugins: dependency-0.6.0, order-1.3.0
collected 1 item                                                                                                                                                 

Config::test_customer_records PASSED                                                                                                                      [100%] 

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

可以返回一个生成数据的函数,然后可以在测试中多次调用此函数。

Fixture 的作用域

Fixtures 可以设置为 5 个不同的作用域,分别是:

  • session:整个测试会话(即所有模块)只会初始化一次 Fixtures 。
  • package:包(即目录)中只会初始化一次 Fixtures 。
  • module:模块中只会初始化一次 Fixtures 。
  • class:测试组中只会初始化一次 Fixtures 。
  • function:每个测试用例都会初始化一次 Fixtures 。
  1. 设置 Fixtures 作用域为 function
# \Test\test_mod1.py
import pytest


@pytest.fixture(scope="function")
def my_list():
    return [1, 2, 3]

def test_one(my_list):
    my_list.append(4)
    assert my_list == [1, 2, 3, 4]


def test_two(my_list):
    assert my_list == [1, 2, 3]


def test_three(my_list):
    assert my_list == [1, 2, 3]

运行结果:

===================================================================== test session starts ======================================================================
configfile: pytest.ini
plugins: dependency-0.6.0, order-1.3.0
collected 3 items                                                                                                                                                

Config::test_one PASSED                                                                                                                                   [ 33%] 
Config::test_two PASSED                                                                                                                                   [ 66%] 
Config::test_three PASSED                                                                                                                                 [100%] 

====================================================================== 3 passed in 0.01s ======================================================================= 

每一个测试用例请求 Fixtures:my_list ,都会初始化 my_list 的值,每个测试用例接收的值都是 [1, 2, 3] 。

  1. 设置 Fixture 作用域为 module
# \Test\test_mod1.py
import pytest

@pytest.fixture(scope="module")
def my_list():
    return [1, 2, 3]

def test_one(my_list):
    my_list.append(4)
    assert my_list == [1, 2, 3, 4]


def test_two(my_list):
    assert my_list == [1, 2, 3]


def test_three(my_list):
    assert my_list == [1, 2, 3]

运行结果:

===================================================================== test session starts ======================================================================
configfile: pytest.ini
plugins: dependency-0.6.0, order-1.3.0
collected 3 items                                                                                                                                                

Config::test_one PASSED                                                                                                                                   [ 33%] 
Config::test_two FAILED                                                                                                                                   [ 66%]
Config::test_three FAILED                                                                                                                                 [100%] 

=================================================================== short test summary info ==================================================================== 
FAILED Config::test_two - AssertionError: assert [1, 2, 3, 4] == [1, 2, 3]
FAILED Config::test_three - AssertionError: assert [1, 2, 3, 4] == [1, 2, 3]
================================================================= 2 failed, 1 passed in 0.04s ================================================================== 

由于 Fixtures:my_list 在整个模块中只会初始化一次,执行 test_one 后,my_list 的值:[1, 2, 3, 4] 被缓存,后续执行 test_two、test_three 时,my_list 的值都是缓存值:[1, 2, 3, 4] 。

其它作用域的逻辑与 module 一致,只是作用域不同,这里不再赘述。

在 Fixture 使用 yield

在 Fixture 里,可以使用 yield 代替 return,yield 相比 return 多了执行 yield 后代码的功能。

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

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

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

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

# \Test\test_mod1.py
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 ======================================================================
configfile: pytest.ini
plugins: dependency-0.6.0, order-1.3.0
collected 2 items                                                                                                                                                

Config::test_one PASSED                                                                                                                                   [ 50%] 
Config::test_two FAILED                                                                                                                                   [100%]

=================================================================== short test summary info ==================================================================== 
FAILED Config::test_two - AssertionError: 测试失败
================================================================= 1 failed, 1 passed in 0.03s ================================================================== 

yield 语句不仅返回值,还在测试完成后执行后续的代码。

但是有一个问题,如果在 yield 出现了错误,那么 yield 之后的代码不会被执行,即使你已经对数据库执行的操作。比如这样:

# \Test\test_mod1.py
import pytest

# 使用字典模拟数据库
user = {}
@pytest.fixture()
def add_user():
    user['user1'] = 'JZY'

@pytest.fixture()
def select_user():
    raise Exception('数据库异常')
    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 ======================================================================
configfile: pytest.ini
plugins: dependency-0.6.0, order-1.3.0
collected 2 items                                                                                                                                                

Config::test_one ERROR                                                                                                                                    [ 50%] 
Config::test_two PASSED                                                                                                                                   [100%] 

=================================================================== short test summary info ==================================================================== 
ERROR Config::test_one - Exception: 数据库异常
================================================================== 1 passed, 1 error in 0.01s ================================================================== 

这样的情况下,设想的后续操作并不会被执行,导致 test_two 通过。

那么我们如何尽量避免这种情况发生呢?在状态更改的事务中使用 yield 回溯期望的状态,并确保状态更改的事务不会引发错误:

# \Test\test_mod1.py
import pytest

# 使用字典模拟数据库
user = {}
@pytest.fixture()
def add_user():
    user['user1'] = 'JZY'
    yield 1
    user.pop('user1')

@pytest.fixture()
def select_user():
    raise Exception('数据库异常')
    return user['user1']


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

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

运行结果:

===================================================================== test session starts ======================================================================
configfile: pytest.ini
plugins: dependency-0.6.0, order-1.3.0
collected 2 items                                                                                                                                                

Config::test_one ERROR                                                                                                                                    [ 50%] 
Config::test_two FAILED                                                                                                                                   [100%]

=================================================================== short test summary info ==================================================================== 
FAILED Config::test_two - AssertionError: 测试失败
ERROR Config::test_one - Exception: 数据库异常
================================================================== 1 failed, 1 error in 0.03s ================================================================== 

无论其它地方是否发生错误,add_user 函数都会运行测试用例后执行清理操作。现在 test_two 如预期一致,未通过测试。

传递参数给 Fixture

Mark 标记传参

# \Test\test_mod1.py
import pytest

@pytest.fixture
def fixt(request):
    marker = request.node.get_closest_marker("user")
    if marker is None:
        # 未获取到标记,则返回None
        data = None
    else:
        # 获取到标记,则返回标记数据
        data = marker.args[0]

    return data

# 已在 pytest.ini 中配置了 user 标记
@pytest.mark.user(42)
def test_fixt(fixt):
    assert fixt == 42

python .\app.py ,运行结果:

===================================================================== test session starts ======================================================================
configfile: pytest.ini
plugins: dependency-0.6.0, order-1.3.0
collected 1 item                                                                                                                                                 

Config::test_fixt PASSED                                                                                                                                  [100%] 

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

定义 Fixture 时,参数化数据

# C:\PythonTest\Package\fixture_fun.py
import pytest


@pytest.fixture(params=["JZY", "one man", pytest.param("a good person", marks=pytest.mark.user)])
def fixt(request):
    """
    通过 request.param 获取参数值
    """
    return request.param

在测试用例中使用 Fixtures:

# C:\PythonTest\Test\test_module1.py
from Package.fixture_fun import fixt

def test_fixt(fixt):
    assert (fixt == "JZY") or (fixt == "one man") or (fixt == "a good person")

运行结果:

===================================================================== test session starts ======================================================================
configfile: pytest.ini
plugins: dependency-0.6.0, order-1.3.0
collected 3 items                                                                                                                                                

Config::test_fixt[JZY] PASSED                                                                                                                             [ 33%] 
Config::test_fixt[one man] PASSED                                                                                                                         [ 66%] 
Config::test_fixt[a good person] PASSED                                                                                                                   [100%] 

====================================================================== 3 passed in 0.01s ======================================================================= 

三个参数就是三个测试,也可以通过 -m user 只执行 a good person 这个参数。

依赖 Fixture 可以实现一些通用操作,比如清理测试环境、初始化测试数据等等。

还有一些 Fixtures 的用法没有在文章中说明,感兴趣的读者可以访问 How to use fixtures 了解,里面有很详细的介绍,可以进阶学习。


THEEND



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