何时使用 pytest 夹具



我是测试新手,偶然发现了pytest灯具,但我不完全确定何时使用它们以及为什么它们有用。

例如,请参阅以下代码:

import pytest
@pytest.fixture
def input_value():
input = 39
return input
def test_divisible_by_3(input_value):
assert input_value % 3 == 0
def test_divisible_by_6(input_value):
assert input_value % 6 == 0

pytest.fixture 在这里有什么功能?为什么我们不能简单地创建一个名为input_value()的函数并在测试函数中运行该函数?例如:

import pytest
def input_value():
input = 39
return input
def test_divisible_by_3():
assert input_value() % 3 == 0
def test_divisible_by_6():
assert input_value() % 6 == 0

为什么我们不能这样做?使用夹具有什么用?

Pytest 夹具和常规函数都可用于构建以测试代码并减少代码重复。乔治·乌多森(George Udoseen)提供的答案很好地解释了这一点。

但是,OP特别询问了pytest.fixture和常规Python函数之间的差异,并且存在许多差异:

Pytest 灯具的作用

域默认情况下,会为引用夹具的每个测试函数执行pytest.fixture。但是,在某些情况下,夹具设置可能计算成本高昂或耗时,例如初始化数据库。为此,可以为pytest.fixture配置更大的范围。这允许在模块(模块范围)中的测试甚至 pytest 运行的所有测试(会话范围)中重用pytest.fixture。以下示例使用模块范围的夹具来加快测试速度:

from time import sleep
import pytest

@pytest.fixture(scope="module")
def expensive_setup():
return sleep(10)
def test_a(expensive_setup):
pass  # expensive_setup is instantiated for this test
def test_b(expensive_setup):
pass  # Reuses expensive_setup, no need to wait 10s

尽管可以通过常规函数调用实现不同的范围,但作用域夹具使用起来要愉快得多。

Pytest 夹具基于依赖注入

Pytest 在测试收集阶段注册所有夹具。当测试函数需要名称与注册的夹具名称匹配的参数时,Pytest 将负责为测试实例化夹具,并将实例提供给测试函数。这是依赖注入的一种形式。

与常规函数相比,它的优点是可以按名称引用任何pytest.fixture,而无需显式导入它。例如,Pytest 附带了一个tmp_path夹具,任何测试都可以使用它来处理临时文件。以下示例取自 Pytest 文档:

CONTENT = "content"

def test_create_file(tmp_path):
d = tmp_path / "sub"
d.mkdir()
p = d / "hello.txt"
p.write_text(CONTENT)
assert p.read_text() == CONTENT
assert len(list(tmp_path.iterdir())) == 1
assert 0

用户在使用前不必导入tmp_path这一事实非常方便。

甚至可以将夹具应用于测试功能,而无需测试函数请求它(请参阅自动使用夹具)。

Pytest 夹具可以参数化

与测试参数化非常相似,夹具参数化允许用户指定夹具的多个"变体",每个变体都有不同的返回值。使用该夹具的每个测试都将执行多次,每个变体执行一次。假设您要测试所有代码是否都针对HTTP和HTTPS URL进行了测试,则可以执行以下操作:

import pytest

@pytest.fixture(params=["http", "https"])
def url_scheme(request):
return request.param

def test_get_call_succeeds(url_scheme):
# Make some assertions
assert True

参数化的夹具将导致每个参考测试与夹具的每个版本一起执行:

$ pytest
tests/test_fixture_param.py::test_get_call_succeeds[http] PASSED                                                                                                                                                                         [ 50%]
tests/test_fixture_param.py::test_get_call_succeeds[https] PASSED                                                                                                                                                                        [100%]
======== 2 passed in 0.01s ========

结论

与常规函数调用相比,Pytest 灯具提供了许多生活质量改进。我建议始终首选 Pytest 灯具而不是常规函数,除非您必须能够直接调用该灯具。直接调用 pytest 灯具不会丢失,调用将失败。

我自己刚开始 pytest,但我知道它减少了编写多次测试使用的代码的需要,因为在这种情况下,您需要多次重写该函数,并且此代码片段将是一个启动器:

夹具用于将一些数据馈送到测试中,例如数据库 连接、要测试的 URL 和某种输入数据。

因此,与其为每个测试运行相同的代码,我们可以 将夹具函数附加到测试,它将运行并返回 在执行每个测试之前的数据到测试。

-- 来源: https://www.tutorialspoint.com/pytest/pytest_fixtures.htm

通常,在测试之间共享它们通用的资源有助于减少重复。同样,这些夹具函数的返回值可以作为"输入参数"传递到各个测试中,如下所示:

@pytest.fixture
def input_value():
input = 39
return input
def test_divisible_by_3(input_value):
assert input_value % 3 == 0

使用夹具:

  • 具有可以多次使用的特定功能并简化测试。

  • 在陈述之前和完成测试后做某事。

例如,Django 中有test_get_username()test_set_check_password(),如下所示:

import pytest
from django.contrib.auth.models import User
@pytest.mark.django_db
def test_get_username():
user = User.objects.create_user("test-user")
user.username = "John"
assert user.get_username() == "John"
@pytest.mark.django_db
def test_set_password_and_check_password():
user = User.objects.create_user("test-user")
user.set_password("new-password")
assert user.check_password("new-password") == True

然后,您可以通过 @pytest.fixture 创建和使用user()以多次使用它并简化test_get_username()test_set_password_and_check_password(),如下所示:

import pytest
from django.contrib.auth.models import User
@pytest.fixture # Here
def user():
user = User.objects.create_user("test-user")
return user
@pytest.mark.django_db
def test_get_username(user): # <- user
user.username = "John"
assert user.get_username() == "John"
@pytest.mark.django_db
def test_set_password_and_check_password(user): # <- user
user.set_password("new-password")
assert user.check_password("new-password") == True

例如,有yieldtest_1()fixture_1(),如下所示:

import pytest
@pytest.fixture
def fixture_1():
print('Before Test')
yield 6
print('After Test')
def test_1(fixture_1):
print('Running Test')
assert fixture_1 == 6

然后,这是下面的输出:

$ pytest -q -rP
.                               [100%]
=============== PASSES ===============
_______________ test_1 _______________
------- Captured stdout setup --------
Before Test
-------- Captured stdout call --------
Running Test
------ Captured stdout teardown ------
After Test
1 passed in 0.10s

此外,您可以运行多个夹具进行test_1()test_2(),如下所示。 *我的答案解释了如何在 Pytest 中从另一个夹具调用夹具,我的答案解释了如何将参数或参数传递给 Pytest 中的夹具,我的答案解释了如何使用 @pytest.mark.parametrize() 的夹具,我的答案解释了如何在@pytest.mark.parametrize()中使用夹具作为参数:

import pytest
@pytest.fixture
def fixture_1():
return "fixture_1"
@pytest.fixture
def fixture_2():
return "fixture_2"
def test_1(fixture_1, fixture_2):
print(fixture_1, fixture_2)
assert True
def test_2(fixture_1, fixture_2):
print(fixture_1, fixture_2)
assert True

然后,这是下面的输出:

$ pytest -q -rP
..                               [100%]
=============== PASSES ================ 
_______________ test_1 ________________ 
-------- Captured stdout call --------- 
fixture_1 fixture_2
_______________ test_2 ________________ 
-------- Captured stdout call --------- 
fixture_1 fixture_2
2 passed in 0.33s

并且,您可以使用request.getfixturevalue()在test_1()test_2()中运行fixture_1()fixture_2(),如下所示:

import pytest
@pytest.fixture
def fixture_1():
return "fixture_1"
@pytest.fixture
def fixture_2():
return "fixture_2"
def test_1(request):
print(request.getfixturevalue("fixture_1"))
print(request.getfixturevalue("fixture_2"))
assert True
def test_2(request):
print(request.getfixturevalue("fixture_1"))
print(request.getfixturevalue("fixture_2"))
assert True

然后,这是下面的输出:

$ pytest -q -rP
..                               [100%]
=============== PASSES ================ 
_______________ test_1 ________________ 
-------- Captured stdout call --------- 
fixture_1
fixture_2
_______________ test_2 ________________ 
-------- Captured stdout call --------- 
fixture_1
fixture_2
2 passed in 0.10s

而且,您可以使用@pytest.fixture(autouse=True)为所有测试运行夹具test_1()test_2(),而无需将夹具设置为测试参数,如下所示。 *测试不能在没有夹具参数的情况下使用夹具的返回值:

import pytest
@pytest.fixture(autouse=True)
def fixture_1():
print("fixture_1")
@pytest.fixture(autouse=True)
def fixture_2():
print("fixture_2")
def test_1():
assert True
def test_2():
assert True

然后,这是下面的输出:

$ pytest -q -rP
..                               [100%]
=============== PASSES ================ 
_______________ test_1 ________________ 
-------- Captured stdout setup -------- 
fixture_1
fixture_2
_______________ test_2 ________________ 
-------- Captured stdout setup -------- 
fixture_1
fixture_2
2 passed in 0.33s

而且,您可以使用 @pytest.mark.usefixtures 进行test_1()test_2()来运行多个夹具,如下所示。 *测试不能在没有夹具参数的情况下使用夹具的返回值:

import pytest
@pytest.fixture
def fixture_1():
print("fixture_1")
@pytest.fixture
def fixture_2():
print("fixture_2")
@pytest.mark.usefixtures('fixture_1', 'fixture_2')
def test_1():
assert True
@pytest.mark.usefixtures('fixture_1', 'fixture_2')
def test_2():
assert True

然后,这是下面的输出:

$ pytest -q -rP
..                               [100%]
=============== PASSES ================ 
_______________ test_1 ________________ 
-------- Captured stdout setup -------- 
fixture_1
fixture_2
_______________ test_2 ________________ 
-------- Captured stdout setup -------- 
fixture_1
fixture_2
2 passed in 0.33s

而且,您可以在类中使用@pytest.mark.usefixturestest_1()test_2()Test来运行多个夹具,如下所示。 *夹具可以使用request.cls.something将数据传递给类中的测试:

import pytest
@pytest.fixture
def fixture_1(request):
request.cls.first_name = "John"
@pytest.fixture
def fixture_2(request):
request.cls.last_name = "Smith"
@pytest.mark.usefixtures('fixture_1', 'fixture_2')
class Test:
def test_1(self):
print(self.first_name, self.last_name)
assert True
def test_2(self):
print(self.first_name, self.last_name)
assert True

然后,这是下面的输出:

$ pytest -q -rP
..                               [100%]
=============== PASSES ================ 
_____________ Test.test_1 _____________ 
-------- Captured stdout call --------- 
John Smith
_____________ Test.test_2 _____________ 
-------- Captured stdout call --------- 
John Smith
2 passed in 0.32s

而且,您可以使用@pytest.fixture(autouse=True)运行Test类中test_1()test_2()的所有测试的夹具,而无需将夹具设置为测试参数,如下所示。 *夹具可以使用request.cls.something将数据传递给类中的测试:

import pytest
@pytest.fixture
def fixture_1(request):
request.cls.first_name = "John"
@pytest.fixture
def fixture_2(request):
request.cls.last_name = "Smith"
@pytest.mark.usefixtures('fixture_1', 'fixture_2')
class Test:
def test_1(self):
print(self.first_name, self.last_name)
assert True
def test_2(self):
print(self.first_name, self.last_name)
assert True

然后,这是下面的输出:

$ pytest -q -rP
..                               [100%]
=============== PASSES ================ 
_____________ Test.test_1 _____________ 
-------- Captured stdout call --------- 
John Smith
_____________ Test.test_2 _____________ 
-------- Captured stdout call --------- 
John Smith
2 passed in 0.33s

据我所知,函数和夹具之间最重要的区别在于,夹具允许您为测试设置一些东西,可能包含状态,然后在测试结束时将其拆除。

SO问题的答案什么是编程中的夹具?对此有些明确。

如果没有需要设置然后拆除的状态,那么使用夹具就没有根本优势,除了一些语法糖。

相关内容

  • 没有找到相关文章

最新更新