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 时,会将数据返回给测试用例;最后测试用例执行结束后,在运行拆卸代码。
我们先设想一个测试流程:
- 新建用户
- 查询数据库,是否新建成功
- 清理新建的用户
利用 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