
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 。
- 设置 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] 。
- 设置 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 后面的代码。
我们先设想一个测试流程:
- 新建用户
- 查询数据库,是否新建成功
- 清理新建的用户
利用 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