Как использовать фикстуры
См. также
См. также
«Запрос» фикстур
В простейшем случае тестовые функции запрашивают необходимые им фикстуры, объявляя их в качестве аргументов.
Когда pytest запускает тест, он проверяет параметры в сигнатуре этой тестовой функции и ищет фикстуры с именами, совпадающими с именами этих параметров. Найдя фикстуры, pytest запускает их, сохраняет возвращённые ими значения (если они есть) и передаёт эти объекты тестовой функции в качестве аргументов.
Краткий пример
import pytest
class Fruit:
def __init__(self, name):
self.name = name
self.cubed = False
def cube(self):
self.cubed = True
class FruitSalad:
def __init__(self, *fruit_bowl):
self.fruit = fruit_bowl
self._cube_fruit()
def _cube_fruit(self):
for fruit in self.fruit:
fruit.cube()
# Arrange
@pytest.fixture
def fruit_bowl():
return [Fruit("apple"), Fruit("banana")]
def test_fruit_salad(fruit_bowl):
# Act
fruit_salad = FruitSalad(*fruit_bowl)
# Assert
assert all(fruit.cubed for fruit in fruit_salad.fruit)
В этом примере test_fruit_salad «requests» fruit_bowl (то есть def test_fruit_salad(fruit_bowl):), и, когда pytest видит это, он выполняет фикстуру fruit_bowl и передаёт возвращённый ею объект в test_fruit_salad в качестве аргумента fruit_bowl.
Примерно вот что происходит, если выполнить всё вручную:
def fruit_bowl():
return [Fruit("apple"), Fruit("banana")]
def test_fruit_salad(fruit_bowl):
# Act
fruit_salad = FruitSalad(*fruit_bowl)
# Assert
assert all(fruit.cubed for fruit in fruit_salad.fruit)
# Arrange
bowl = fruit_bowl()
test_fruit_salad(fruit_bowl=bowl)
Фикстуры могут запрашивать другие фикстуры
Одна из главных сильных сторон pytest — чрезвычайно гибкая система фикстур. Она позволяет свести сложные требования тестов к более простым и организованным функциям, каждая из которых описывает только то, от чего зависит. Мы подробнее рассмотрим это далее, а пока приведём краткий пример того, как фикстуры могут использовать другие фикстуры:
# contents of test_append.py
import pytest
# Arrange
@pytest.fixture
def first_entry():
return "a"
# Arrange
@pytest.fixture
def order(first_entry):
return [first_entry]
def test_string(order):
# Act
order.append("b")
# Assert
assert order == ["a", "b"]
Обратите внимание: это тот же пример, что и выше, но изменилось совсем немногое. Фикстуры в pytest запрашивают фикстуры так же, как это делают тесты. К фикстурам применяются те же правила запроса, что и к тестам. Вот как этот пример работал бы при ручном выполнении:
def first_entry():
return "a"
def order(first_entry):
return [first_entry]
def test_string(order):
# Act
order.append("b")
# Assert
assert order == ["a", "b"]
entry = first_entry()
the_list = order(first_entry=entry)
test_string(order=the_list)
Фикстуры можно использовать повторно
Система фикстур pytest настолько мощная ещё и потому, что позволяет определять универсальный шаг настройки, который можно многократно использовать, как обычную функцию. Два разных теста могут запросить одну и ту же фикстуру, и pytest предоставит каждому тесту отдельный результат её выполнения.
Это очень полезно для того, чтобы тесты не влияли друг на друга. С помощью этой системы можно гарантировать, что каждый тест получит собственный набор данных и начнёт работу в чистом состоянии, чтобы результаты были согласованными и воспроизводимыми.
Вот пример того, когда это может пригодиться:
# contents of test_append.py
import pytest
# Arrange
@pytest.fixture
def first_entry():
return "a"
# Arrange
@pytest.fixture
def order(first_entry):
return [first_entry]
def test_string(order):
# Act
order.append("b")
# Assert
assert order == ["a", "b"]
def test_int(order):
# Act
order.append(2)
# Assert
assert order == ["a", 2]
Каждому тесту здесь передаётся собственная копия объекта list, а это значит, что фикстура order выполняется дважды (то же самое относится к фикстуре first_entry). Если выполнить это вручную, код выглядел бы примерно так:
def first_entry():
return "a"
def order(first_entry):
return [first_entry]
def test_string(order):
# Act
order.append("b")
# Assert
assert order == ["a", "b"]
def test_int(order):
# Act
order.append(2)
# Assert
assert order == ["a", 2]
entry = first_entry()
the_list = order(first_entry=entry)
test_string(order=the_list)
entry = first_entry()
the_list = order(first_entry=entry)
test_int(order=the_list)
Тест или фикстура может запрашивать несколько фикстур одновременно
Тесты и фикстуры не ограничены запросом только одной фикстуры за раз. Они могут запрашивать сколько угодно фикстур. Вот ещё один краткий пример:
# contents of test_append.py
import pytest
# Arrange
@pytest.fixture
def first_entry():
return "a"
# Arrange
@pytest.fixture
def second_entry():
return 2
# Arrange
@pytest.fixture
def order(first_entry, second_entry):
return [first_entry, second_entry]
# Arrange
@pytest.fixture
def expected_list():
return ["a", 2, 3.0]
def test_string(order, expected_list):
# Act
order.append(3.0)
# Assert
assert order == expected_list
Фикстуру можно запрашивать несколько раз в одном тесте (возвращаемые значения кэшируются)
Фикстуры можно запрашивать несколько раз в рамках одного теста, и pytest не будет выполнять их повторно для этого теста. Это означает, что мы можем запрашивать фикстуры в нескольких зависящих от них фикстурах (и даже повторно в самом тесте), не вызывая повторного выполнения этих фикстур.
# contents of test_append.py
import pytest
# Arrange
@pytest.fixture
def first_entry():
return "a"
# Arrange
@pytest.fixture
def order():
return []
# Act
@pytest.fixture
def append_first(order, first_entry):
return order.append(first_entry)
def test_string_only(append_first, order, first_entry):
# Assert
assert order == [first_entry]
Если бы запрошенная фикстура выполнялась при каждом её запросе во время теста, этот тест завершился бы ошибкой: и append_first, и test_string_only получили бы order в виде пустого списка (то есть []). Но, поскольку после первого вызова возвращаемое значение order было кэшировано (вместе с любыми побочными эффектами, которые могли возникнуть при её выполнении), тест и append_first ссылались на один и тот же объект, и тест увидел влияние append_first на этот объект.
Автоматически используемые фикстуры (фикстуры, которые не нужно запрашивать)
Иногда бывает нужно использовать фикстуру (или даже несколько), от которой зависят все ваши тесты. Фикстуры с автоматическим использованием (autouse) позволяют автоматически запрашивать их во всех тестах. Это избавляет от множества повторяющихся запросов и даже открывает возможности для более продвинутого использования фикстур (об этом подробнее далее).
Чтобы сделать фикстуру автоматически используемой, передайте autouse=True декоратору фикстуры. Вот простой пример их использования:
# contents of test_append.py
import pytest
@pytest.fixture
def first_entry():
return "a"
@pytest.fixture
def order(first_entry):
return []
@pytest.fixture(autouse=True)
def append_first(order, first_entry):
return order.append(first_entry)
def test_string_only(order, first_entry):
assert order == [first_entry]
def test_string_and_int(order, first_entry):
order.append(2)
assert order == [first_entry, 2]
В этом примере фикстура append_first используется автоматически. Поскольку это происходит автоматически, она влияет на оба теста, хотя ни один из них её не запрашивает. Это не значит, что её нельзя запросить; просто в этом нет необходимости.
Область действия: совместное использование фикстур в классах, модулях, пакетах или в течение сессии
Фикстуры, которым требуется доступ к сети, зависят от подключения и обычно требуют много времени для создания. Продолжая предыдущий пример, можно добавить параметр scope="module" в вызов @pytest.fixture, чтобы функция фикстуры smtp_connection, отвечающая за подключение к уже существующему SMTP-серверу, вызывалась только один раз на модуль тестов (по умолчанию она вызывается один раз на тестовую функцию). Таким образом, несколько тестовых функций в одном модуле будут получать один и тот же экземпляр фикстуры smtp_connection, что экономит время. Возможные значения для scope: function, class, module, package или session.
В следующем примере функция фикстуры помещена в отдельный файл conftest.py, чтобы тесты из нескольких тестовых модулей в каталоге могли получить к ней доступ:
# content of conftest.py
import smtplib
import pytest
@pytest.fixture(scope="module")
def smtp_connection():
return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
# content of test_module.py
def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehlo()
assert response == 250
assert b"smtp.gmail.com" in msg
assert 0 # for demo purposes
def test_noop(smtp_connection):
response, msg = smtp_connection.noop()
assert response == 250
assert 0 # for demo purposes
Здесь для test_ehlo требуется значение фикстуры smtp_connection. pytest найдёт и вызовет функцию фикстуры smtp_connection, помеченную @pytest.fixture. Запуск теста выглядит так:
$ pytest test_module.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-9.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 2 items
test_module.py FF [100%]
================================= FAILURES =================================
________________________________ test_ehlo _________________________________
smtp_connection = <smtplib.SMTP object at 0xdeadbeef0001>
def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehlo()
assert response == 250
assert b"smtp.gmail.com" in msg
> assert 0 # for demo purposes
^^^^^^^^
E assert 0
test_module.py:7: AssertionError
________________________________ test_noop _________________________________
smtp_connection = <smtplib.SMTP object at 0xdeadbeef0001>
def test_noop(smtp_connection):
response, msg = smtp_connection.noop()
assert response == 250
> assert 0 # for demo purposes
^^^^^^^^
E assert 0
test_module.py:13: AssertionError
========================= short test summary info ==========================
FAILED test_module.py::test_ehlo - assert 0
FAILED test_module.py::test_noop - assert 0
============================ 2 failed in 0.12s =============================
Видно, что оба теста assert 0 завершаются с ошибкой, и, что важнее, в обе тестовые функции был передан абсолютно один и тот же объект smtp_connection: pytest показывает полученные значения аргументов в трассировке. Поэтому две тестовые функции, использующие smtp_connection, выполняются так же быстро, как одна: они повторно используют один и тот же экземпляр.
Если вместо этого вам нужен экземпляр smtp_connection с областью действия на всю сессию, достаточно объявить его:
@pytest.fixture(scope="session")
def smtp_connection():
# the returned fixture value will be shared for
# all tests requesting it
...
Области действия фикстур
Фикстуры создаются при первом запросе тестом и уничтожаются в соответствии с их scope:
-
function: область действия по умолчанию; фикстура уничтожается в конце теста. -
class: фикстура уничтожается при завершении последнего теста в классе. -
module: фикстура уничтожается при завершении последнего теста в модуле. -
package: фикстура уничтожается при завершении последнего теста в пакете, где она определена, включая вложенные пакеты и подкаталоги. -
session: фикстура уничтожается в конце тестовой сессии.
Примечание
pytest кэширует только один экземпляр фикстуры за раз. Поэтому при использовании параметризованной фикстуры pytest может вызвать фикстуру несколько раз в заданной области действия.
Динамическая область действия
Добавлено в версии 5.2.
В некоторых случаях может понадобиться изменить область действия фикстуры, не меняя код. Для этого передайте вызываемый объект в scope. Он должен возвращать строку с допустимой областью действия и будет выполнен только один раз — при определении фикстуры. Он будет вызван с двумя именованными аргументами: fixture_name в виде строки и config с объектом конфигурации.
Это особенно полезно при работе с фикстурами, настройка которых требует времени, например при запуске контейнера Docker. Аргумент командной строки можно использовать для управления областью действия запущенных контейнеров в разных средах. См. пример ниже.
def determine_scope(fixture_name, config):
if config.getoption("--keep-containers", None):
return "session"
return "function"
@pytest.fixture(scope=determine_scope)
def docker_container():
yield spawn_container()
Завершение работы и очистка (то есть финализация фикстур)
При запуске тестов важно убедиться, что они очищают за собой, чтобы не влиять на другие тесты (и не оставлять после себя горы тестовых данных, раздувающих систему). В pytest есть очень полезная система завершения работы, позволяющая определить конкретные шаги, которые должна выполнить каждая фикстура для очистки за собой.
Эту систему можно использовать двумя способами.
1. Фикстуры с yield (рекомендуемый способ)
Фикстуры с «yield» yield вместо return. Такие фикстуры позволяют выполнить код и передать объект запрашивающей фикстуре или тесту, как и обычные фикстуры. Отличия только в следующем:
-
Вместо
returnиспользуетсяyield. - Код очистки для этой фикстуры размещается после
yield.
Определив линейный порядок фикстур, pytest запускает каждую до тех пор, пока она не вернёт значение или не выполнит yield, а затем переходит к следующей фикстуре в списке.
Когда тест завершён, pytest проходит по списку фикстур в обратном порядке, выбирая те, которые выполнили yield, и запускает код, расположенный после инструкции yield.
Рассмотрим простой пример базового модуля для работы с электронной почтой:
# content of emaillib.py
class MailAdminClient:
def create_user(self):
return MailUser()
def delete_user(self, user):
# do some cleanup
pass
class MailUser:
def __init__(self):
self.inbox = []
def send_email(self, email, other):
other.inbox.append(email)
def clear_mailbox(self):
self.inbox.clear()
class Email:
def __init__(self, subject, body):
self.subject = subject
self.body = body
Предположим, что мы хотим проверить отправку письма от одного пользователя другому. Сначала нужно создать каждого пользователя, затем отправить письмо от одного пользователя другому и, наконец, проверить, что второй пользователь получил это сообщение во входящих. Если нужно выполнить очистку после теста, скорее всего, следует убедиться, что почтовый ящик второго пользователя пуст, прежде чем удалять этого пользователя. В противном случае система может выдать ошибку.
Это может выглядеть так:
# content of test_emaillib.py
from emaillib import Email, MailAdminClient
import pytest
@pytest.fixture
def mail_admin():
return MailAdminClient()
@pytest.fixture
def sending_user(mail_admin):
user = mail_admin.create_user()
yield user
mail_admin.delete_user(user)
@pytest.fixture
def receiving_user(mail_admin):
user = mail_admin.create_user()
yield user
user.clear_mailbox()
mail_admin.delete_user(user)
def test_email_received(sending_user, receiving_user):
email = Email(subject="Hey!", body="How's it going?")
sending_user.send_email(email, receiving_user)
assert email in receiving_user.inbox
Поскольку receiving_user — последняя фикстура, выполняемая при настройке, она первой выполняется при завершении работы.
Даже правильный порядок действий при завершении работы не гарантирует безопасную очистку. Подробнее об этом рассказано в разделе Безопасное завершение работы.
$ pytest -q test_emaillib.py . [100%] 1 passed in 0.12s
Обработка ошибок в фикстуре с yield
Если в фикстуре с yield возникает исключение до выполнения yield, pytest не будет запускать код завершения работы, расположенный после инструкции yield этой фикстуры. Однако pytest всё равно попытается завершить работу всех фикстур, которые уже успешно выполнились для этого теста, как обычно.
2. Непосредственное добавление финализаторов
Фикстуры с yield считаются более простым и понятным вариантом, но есть и другой способ: непосредственно добавить функции-финализаторы к объекту контекста запроса теста. Результат будет похож на использование фикстур с yield, но код получится немного многословнее.
Чтобы воспользоваться этим подходом, нужно запросить объект контекста запроса (так же, как мы запросили бы другую фикстуру) в фикстуре, для которой нужно добавить код завершения работы, а затем передать вызываемый объект с этим кодом её методу addfinalizer.
Однако нужно быть осторожными: pytest выполнит финализатор после его добавления, даже если фикстура вызовет исключение уже после этого. Поэтому, чтобы не запускать код финализатора без необходимости, добавлять его следует только после выполнения в фикстуре действий, требующих завершения работы.
Вот как выглядел бы предыдущий пример при использовании метода addfinalizer:
# content of test_emaillib.py
from emaillib import Email, MailAdminClient
import pytest
@pytest.fixture
def mail_admin():
return MailAdminClient()
@pytest.fixture
def sending_user(mail_admin):
user = mail_admin.create_user()
yield user
mail_admin.delete_user(user)
@pytest.fixture
def receiving_user(mail_admin, request):
user = mail_admin.create_user()
def delete_user():
mail_admin.delete_user(user)
request.addfinalizer(delete_user)
return user
@pytest.fixture
def email(sending_user, receiving_user, request):
_email = Email(subject="Hey!", body="How's it going?")
sending_user.send_email(_email, receiving_user)
def empty_mailbox():
receiving_user.clear_mailbox()
request.addfinalizer(empty_mailbox)
return _email
def test_email_received(receiving_user, email):
assert email in receiving_user.inbox
Этот способ немного длиннее и сложнее фикстур с yield, но в некоторых затруднительных ситуациях он предлагает дополнительные возможности.
$ pytest -q test_emaillib.py . [100%] 1 passed in 0.12s
Примечание о порядке выполнения финализаторов
Финализаторы выполняются в обратном порядке: последним добавлен — первым выполнен. Для фикстур с yield первым запускается код завершения работы самой правой фикстуры, то есть последнего параметра теста.
# content of test_finalizers.py
import pytest
def test_bar(fix_w_yield1, fix_w_yield2):
print("test_bar")
@pytest.fixture
def fix_w_yield1():
yield
print("after_yield_1")
@pytest.fixture
def fix_w_yield2():
yield
print("after_yield_2")
$ pytest -s test_finalizers.py =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-9.x.y, pluggy-1.x.y rootdir: /home/sweet/project collected 1 item test_finalizers.py test_bar .after_yield_2 after_yield_1 ============================ 1 passed in 0.12s =============================
Для финализаторов первым выполняется последний вызов request.addfinalizer.
# content of test_finalizers.py
from functools import partial
import pytest
@pytest.fixture
def fix_w_finalizers(request):
request.addfinalizer(partial(print, "finalizer_2"))
request.addfinalizer(partial(print, "finalizer_1"))
def test_bar(fix_w_finalizers):
print("test_bar")
$ pytest -s test_finalizers.py =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-9.x.y, pluggy-1.x.y rootdir: /home/sweet/project collected 1 item test_finalizers.py test_bar .finalizer_1 finalizer_2 ============================ 1 passed in 0.12s =============================
Это связано с тем, что фикстуры с yield используют addfinalizer: при выполнении фикстуры addfinalizer регистрирует функцию, возобновляющую работу генератора, который, в свою очередь, вызывает код завершения работы.
Безопасное завершение работы
Система фикстур pytest очень мощная, но она всё равно управляется компьютером, который не способен самостоятельно понять, как безопасно завершить работу всего, что мы ему поручаем. Если не соблюдать осторожность, ошибка в неподходящем месте может оставить после тестов неочищенные данные, что довольно быстро приведёт к новым проблемам.
Например, рассмотрим следующие тесты (на основе приведённого выше примера с электронной почтой):
# content of test_emaillib.py
from emaillib import Email, MailAdminClient
import pytest
@pytest.fixture
def setup():
mail_admin = MailAdminClient()
sending_user = mail_admin.create_user()
receiving_user = mail_admin.create_user()
email = Email(subject="Hey!", body="How's it going?")
sending_user.send_email(email, receiving_user)
yield receiving_user, email
receiving_user.clear_mailbox()
mail_admin.delete_user(sending_user)
mail_admin.delete_user(receiving_user)
def test_email_received(setup):
receiving_user, email = setup
assert email in receiving_user.inbox
Этот вариант гораздо компактнее, но его сложнее читать, имя фикстуры не очень информативно, а сами фикстуры трудно использовать повторно.
Есть и более серьёзная проблема: если на каком-либо этапе настройки возникнет исключение, код завершения работы не будет выполнен.
Можно было бы вместо фикстур с yield использовать метод addfinalizer, но он может оказаться довольно сложным и трудным в сопровождении (и уже не будет таким компактным).
$ pytest -q test_emaillib.py . [100%] 1 passed in 0.12s
Безопасная структура фикстур
Самая безопасная и простая структура фикстур предполагает, что каждая фикстура выполняет только одно действие, изменяющее состояние, а затем они объединяются со своим кодом завершения работы, как показано в приведённых выше примерах с электронной почтой.
Вероятность того, что операция, изменяющая состояние, завершится ошибкой, но всё же успеет изменить состояние, ничтожно мала: большинство таких операций основано на транзакциях (по крайней мере, на уровне тестирования, при котором состояние может остаться неочищенным). Поэтому, если мы будем гарантировать, что каждое успешно выполненное действие, изменяющее состояние, очищается, вынося его в отдельную функцию-фикстуру и отделяя от других потенциально завершающихся ошибкой действий, изменяющих состояние, у наших тестов будет больше шансов оставить тестовую среду в том же состоянии, в котором они её застали.
Предположим, у нас есть веб-сайт со страницей входа, а также доступ к административному API, позволяющему создавать пользователей. Для теста мы хотим:
- Создать пользователя через административный API
- Запустить браузер с помощью Selenium
- Перейти на страницу входа нашего сайта
- Войти под созданным пользователем
- Проверить, что его имя отображается в заголовке целевой страницы
Мы не хотим оставлять этого пользователя в системе или незакрытый сеанс браузера, поэтому нужно убедиться, что фикстуры, создающие эти объекты, очищают за собой.
Это может выглядеть так:
Примечание
В этом примере предполагается, что некоторые фикстуры (то есть base_url и admin_credentials) определены в другом месте. Пока будем считать, что они существуют, и просто не будем их рассматривать.
from uuid import uuid4
from urllib.parse import urljoin
from selenium.webdriver import Chrome
import pytest
from src.utils.pages import LoginPage, LandingPage
from src.utils import AdminApiClient
from src.utils.data_types import User
@pytest.fixture
def admin_client(base_url, admin_credentials):
return AdminApiClient(base_url, **admin_credentials)
@pytest.fixture
def user(admin_client):
_user = User(name="Susan", username=f"testuser-{uuid4()}", password="P4$$word")
admin_client.create_user(_user)
yield _user
admin_client.delete_user(_user)
@pytest.fixture
def driver():
_driver = Chrome()
yield _driver
_driver.quit()
@pytest.fixture
def login(driver, base_url, user):
driver.get(urljoin(base_url, "/login"))
page = LoginPage(driver)
page.login(user)
@pytest.fixture
def landing_page(driver, login):
return LandingPage(driver)
def test_name_on_landing_page_after_login(landing_page, user):
assert landing_page.header == f"Welcome, {user.name}!"
Из структуры зависимостей неясно, выполнится ли фикстура user до фикстуры driver. Но это не проблема: эти операции атомарны, поэтому неважно, какая из них выполнится первой, ведь последовательность событий теста всё равно линеаризуема. Важно другое: независимо от того, какая операция выполнится первой, если одна из них вызовет исключение, а другая могла бы выполниться успешно, ни одна из них не оставит после себя никаких данных. Если driver выполнится до user и user вызовет исключение, браузер всё равно закроется, а пользователь не будет создан. А если исключение вызовет driver, браузер не будет запущен, а пользователь не будет создан.
Безопасный запуск нескольких инструкций assert
Иногда после выполнения всех действий по настройке требуется выполнить несколько проверок. Это имеет смысл, поскольку в более сложных системах одно действие может запускать несколько процессов. В pytest есть удобный способ справиться с этим, объединяющий многие из рассмотренных приёмов.
Нужно лишь перейти к области действия более высокого уровня, определить этап действия в виде автоматически используемой фикстуры и убедиться, что все фикстуры относятся к этой области действия более высокого уровня.
Возьмём пример, приведённый выше, и немного изменим его. Помимо приветственного сообщения в заголовке, предположим, что мы хотим проверить наличие кнопки выхода и ссылки на профиль пользователя.
Посмотрим, как структурировать код, чтобы выполнить несколько проверок и не повторять все эти действия.
Примечание
В этом примере предполагается, что некоторые фикстуры (то есть base_url и admin_credentials) определены в другом месте. Пока будем считать, что они существуют, и просто не будем их рассматривать.
# contents of tests/end_to_end/test_login.py
from uuid import uuid4
from urllib.parse import urljoin
from selenium.webdriver import Chrome
import pytest
from src.utils.pages import LoginPage, LandingPage
from src.utils import AdminApiClient
from src.utils.data_types import User
@pytest.fixture(scope="class")
def admin_client(base_url, admin_credentials):
return AdminApiClient(base_url, **admin_credentials)
@pytest.fixture(scope="class")
def user(admin_client):
_user = User(name="Susan", username=f"testuser-{uuid4()}", password="P4$$word")
admin_client.create_user(_user)
yield _user
admin_client.delete_user(_user)
@pytest.fixture(scope="class")
def driver():
_driver = Chrome()
yield _driver
_driver.quit()
@pytest.fixture(scope="class")
def landing_page(driver, login):
return LandingPage(driver)
class TestLandingPageSuccess:
@pytest.fixture(scope="class", autouse=True)
@classmethod
def login(cls, driver, base_url, user):
driver.get(urljoin(base_url, "/login"))
page = LoginPage(driver)
page.login(user)
def test_name_in_header(self, landing_page, user):
assert landing_page.header == f"Welcome, {user.name}!"
def test_sign_out_button(self, landing_page):
assert landing_page.sign_out_button.is_displayed()
def test_profile_link(self, landing_page, user):
profile_href = urljoin(base_url, f"/profile?id={user.profile_id}")
assert landing_page.profile_link.get_attribute("href") == profile_href
Обратите внимание, что методы указывают self в сигнатуре лишь для формальности. Состояние не связано с самим тестовым классом, как это может быть в фреймворке unittest.TestCase. Всё управляется системой фикстур pytest.
Каждый метод запрашивает только те фикстуры, которые ему действительно нужны, и ему не нужно беспокоиться о порядке. Это возможно потому, что фикстура действия используется автоматически и гарантирует, что все остальные фикстуры выполнятся до неё. Больше не требуется изменять состояние, поэтому тесты могут выполнять сколько угодно запросов, не меняющих состояние, не рискуя помешать другим тестам.
Фикстура login тоже определена внутри класса, поскольку не каждый из остальных тестов модуля предполагает успешный вход, а для другого тестового класса может потребоваться иначе обработать этап действия. Например, если мы хотим написать другой сценарий тестирования для отправки неверных учётных данных, можно добавить в тестовый файл что-то вроде следующего:
class TestLandingPageBadCredentials:
@pytest.fixture(scope="class")
@classmethod
def faux_user(cls, user):
_user = deepcopy(user)
_user.password = "badpass"
return _user
def test_raises_bad_credentials_exception(self, login_page, faux_user):
with pytest.raises(BadCredentialsException):
login_page.login(faux_user)
Фикстуры могут анализировать контекст запрашивающего теста
Функции-фикстуры могут принимать объект request, чтобы анализировать контекст «запрашивающей» тестовой функции, класса или модуля. Продолжим предыдущий пример с фикстурой smtp_connection и прочитаем необязательный URL сервера из тестового модуля, использующего нашу фикстуру:
# content of conftest.py
import smtplib
import pytest
@pytest.fixture(scope="module")
def smtp_connection(request):
server = getattr(request.module, "smtpserver", "smtp.gmail.com")
smtp_connection = smtplib.SMTP(server, 587, timeout=5)
yield smtp_connection
print(f"finalizing {smtp_connection} ({server})")
smtp_connection.close()
Мы используем атрибут request.module, чтобы при необходимости получить атрибут smtpserver из тестового модуля. При повторном запуске почти ничего не изменится:
$ pytest -s -q --tb=no test_module.py FFfinalizing <smtplib.SMTP object at 0xdeadbeef0002> (smtp.gmail.com) ========================= short test summary info ========================== FAILED test_module.py::test_ehlo - assert 0 FAILED test_module.py::test_noop - assert 0 2 failed in 0.12s
Создадим ещё один тестовый модуль, в пространстве имён которого задан URL сервера:
# content of test_anothersmtp.py
smtpserver = "mail.python.org" # will be read by smtp fixture
def test_showhelo(smtp_connection):
assert 0, smtp_connection.helo()
Запустим его:
$ pytest -qq --tb=short test_anothersmtp.py
F [100%]
================================= FAILURES =================================
______________________________ test_showhelo _______________________________
test_anothersmtp.py:6: in test_showhelo
assert 0, smtp_connection.helo()
E AssertionError: (250, b'mail.python.org')
E assert 0
------------------------- Captured stdout teardown -------------------------
finalizing <smtplib.SMTP object at 0xdeadbeef0003> (mail.python.org)
========================= short test summary info ==========================
FAILED test_anothersmtp.py::test_showhelo - AssertionError: (250, b'mail....
Вуаля! Функция-фикстура smtp_connection получила имя нашего почтового сервера из пространства имён модуля.
Передача данных в фикстуры с помощью маркеров
С помощью объекта request фикстура также может получить доступ к маркерам, применённым к тестовой функции. Это может пригодиться, чтобы передать данные из теста в фикстуру:
import pytest
@pytest.fixture
def fixt(request):
marker = request.node.get_closest_marker("fixt_data")
if marker is None:
# Handle missing marker in some way...
data = None
else:
data = marker.args[0]
# Do something with the data
return data
@pytest.mark.fixt_data(42)
def test_fixt(fixt):
assert fixt == 42
Фабрики в качестве фикстур
Подход «фабрика в качестве фикстуры» может пригодиться, когда результат фикстуры требуется несколько раз в одном тесте. Вместо непосредственного возврата данных фикстура возвращает функцию, которая создаёт эти данные. Эту функцию затем можно вызывать несколько раз в тесте.
При необходимости фабрики могут принимать параметры:
@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")
customer_2 = make_customer_record("Mike")
customer_3 = make_customer_record("Meredith")
Если созданные фабрикой данные требуют управления, фикстура может взять это на себя:
@pytest.fixture
def make_customer_record():
created_records = []
def _make_customer_record(name):
record = models.Customer(name=name, orders=[])
created_records.append(record)
return record
yield _make_customer_record
for record in created_records:
record.destroy()
def test_customer_records(make_customer_record):
customer_1 = make_customer_record("Lisa")
customer_2 = make_customer_record("Mike")
customer_3 = make_customer_record("Meredith")
Параметризация фикстур
Функции-фикстуры можно параметризовать. В этом случае они будут вызываться несколько раз, и каждый раз будут выполняться зависящие от них тесты. Обычно тестовым функциям не нужно знать о повторном запуске. Параметризация фикстур помогает писать исчерпывающие функциональные тесты для компонентов, которые можно настроить разными способами.
Продолжая предыдущий пример, можно указать фикстуре создавать два экземпляра фикстуры smtp_connection, благодаря чему все использующие её тесты будут выполнены дважды. Функция-фикстура получает доступ к каждому параметру через специальный объект request:
# content of conftest.py
import smtplib
import pytest
@pytest.fixture(scope="module", params=["smtp.gmail.com", "mail.python.org"])
def smtp_connection(request):
smtp_connection = smtplib.SMTP(request.param, 587, timeout=5)
yield smtp_connection
print(f"finalizing {smtp_connection}")
smtp_connection.close()
Главное изменение — объявление params с помощью @pytest.fixture: список значений, для каждого из которых будет выполняться функция-фикстура, а получить значение можно через request.param. Код тестовых функций менять не нужно. Запустим тесты ещё раз:
$ pytest -q test_module.py
FFFF [100%]
================================= FAILURES =================================
________________________ test_ehlo[smtp.gmail.com] _________________________
smtp_connection = <smtplib.SMTP object at 0xdeadbeef0004>
def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehlo()
assert response == 250
assert b"smtp.gmail.com" in msg
> assert 0 # for demo purposes
^^^^^^^^
E assert 0
test_module.py:7: AssertionError
________________________ test_noop[smtp.gmail.com] _________________________
smtp_connection = <smtplib.SMTP object at 0xdeadbeef0004>
def test_noop(smtp_connection):
response, msg = smtp_connection.noop()
assert response == 250
> assert 0 # for demo purposes
^^^^^^^^
E assert 0
test_module.py:13: AssertionError
________________________ test_ehlo[mail.python.org] ________________________
smtp_connection = <smtplib.SMTP object at 0xdeadbeef0005>
def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehlo()
assert response == 250
> assert b"smtp.gmail.com" in msg
E AssertionError: assert b'smtp.gmail.com' in b'mail.python.org\nPIPELINING\nSIZE 51200000\nETRN\nSTARTTLS\nAUTH DIGEST-MD5 NTLM CRAM-MD5\nENHANCEDSTATUSCODES\n8BITMIME\nDSN\nSMTPUTF8\nCHUNKING'
test_module.py:6: AssertionError
-------------------------- Captured stdout setup ---------------------------
finalizing <smtplib.SMTP object at 0xdeadbeef0004>
________________________ test_noop[mail.python.org] ________________________
smtp_connection = <smtplib.SMTP object at 0xdeadbeef0005>
def test_noop(smtp_connection):
response, msg = smtp_connection.noop()
assert response == 250
> assert 0 # for demo purposes
^^^^^^^^
E assert 0
test_module.py:13: AssertionError
------------------------- Captured stdout teardown -------------------------
finalizing <smtplib.SMTP object at 0xdeadbeef0005>
========================= short test summary info ==========================
FAILED test_module.py::test_ehlo[smtp.gmail.com] - assert 0
FAILED test_module.py::test_noop[smtp.gmail.com] - assert 0
FAILED test_module.py::test_ehlo[mail.python.org] - AssertionError: asser...
FAILED test_module.py::test_noop[mail.python.org] - assert 0
4 failed in 0.12s
Мы видим, что обе тестовые функции выполнились дважды с разными экземплярами smtp_connection. Обратите также внимание, что при подключении mail.python.org второй тест завершается ошибкой в test_ehlo, поскольку ожидаемая строка сервера отличается от полученной.
Для каждого значения фикстуры в параметризованной фикстуре pytest формирует строку — идентификатор теста, например test_ehlo[smtp.gmail.com] и test_ehlo[mail.python.org] в приведённых выше примерах. Эти идентификаторы можно использовать с параметром -k, чтобы выбрать конкретные тесты для запуска. Кроме того, по ним определяется конкретный тест, завершившийся ошибкой. Запуск pytest с параметром --collect-only покажет созданные идентификаторы.
Для чисел, строк, логических значений и None в идентификаторе теста будет использоваться их обычное строковое представление. Для других объектов pytest сформирует строку на основе имени аргумента. Чтобы настроить строку, используемую в идентификаторе теста для определённого значения фикстуры, можно использовать именованный аргумент ids:
# content of test_ids.py
import pytest
@pytest.fixture(params=[0, 1], ids=["spam", "ham"])
def a(request):
return request.param
def test_a(a):
pass
def idfn(fixture_value):
if fixture_value == 0:
return "eggs"
else:
return None
@pytest.fixture(params=[0, 1], ids=idfn)
def b(request):
return request.param
def test_b(b):
pass
В примере выше показано, что ids может быть списком используемых строк или функцией, которая вызывается со значением фикстуры и должна вернуть используемую строку. Во втором случае, если функция возвращает None, будет использоваться идентификатор, автоматически сгенерированный pytest.
При запуске приведённых выше тестов используются следующие идентификаторы:
$ pytest --collect-only
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-9.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 12 items
<Dir fixtures.rst-236>
<Module test_anothersmtp.py>
<Function test_showhelo[smtp.gmail.com]>
<Function test_showhelo[mail.python.org]>
<Module test_emaillib.py>
<Function test_email_received>
<Module test_finalizers.py>
<Function test_bar>
<Module test_ids.py>
<Function test_a[spam]>
<Function test_a[ham]>
<Function test_b[eggs]>
<Function test_b[1]>
<Module test_module.py>
<Function test_ehlo[smtp.gmail.com]>
<Function test_noop[smtp.gmail.com]>
<Function test_ehlo[mail.python.org]>
<Function test_noop[mail.python.org]>
======================= 12 tests collected in 0.12s ========================
Использование маркеров с параметризованными фикстурами
pytest.param() можно использовать для применения маркеров к наборам значений параметризованных фикстур так же, как и с @pytest.mark.parametrize.
Пример:
# content of test_fixture_marks.py
import pytest
@pytest.fixture(params=[0, 1, pytest.param(2, marks=pytest.mark.skip)])
def data_set(request):
return request.param
def test_data(data_set):
pass
При запуске этого теста вызов data_set со значением 2 будет пропущен:
$ pytest test_fixture_marks.py -v =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-9.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache rootdir: /home/sweet/project collecting ... collected 3 items test_fixture_marks.py::test_data[0] PASSED [ 33%] test_fixture_marks.py::test_data[1] PASSED [ 66%] test_fixture_marks.py::test_data[2] SKIPPED (unconditional skip) [100%] ======================= 2 passed, 1 skipped in 0.12s =======================
Модульность: использование фикстур в функции-фикстуре
Фикстуры можно использовать не только в тестовых функциях: функции-фикстуры также могут использовать другие фикстуры. Это способствует модульной организации фикстур и позволяет повторно использовать специфичные для фреймворка фикстуры в разных проектах. В качестве простого примера расширим предыдущий пример и создадим объект app, в который поместим уже определённый ресурс smtp_connection:
# content of test_appsetup.py
import pytest
class App:
def __init__(self, smtp_connection):
self.smtp_connection = smtp_connection
@pytest.fixture(scope="module")
def app(smtp_connection):
return App(smtp_connection)
def test_smtp_connection_exists(app):
assert app.smtp_connection
Здесь мы объявляем фикстуру app, которая получает ранее определённую фикстуру smtp_connection и создаёт на её основе объект App. Запустим тесты:
$ pytest -v test_appsetup.py =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-9.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache rootdir: /home/sweet/project collecting ... collected 2 items test_appsetup.py::test_smtp_connection_exists[smtp.gmail.com] PASSED [ 50%] test_appsetup.py::test_smtp_connection_exists[mail.python.org] PASSED [100%] ============================ 2 passed in 0.12s =============================
Из-за параметризации smtp_connection тест выполнится дважды с разными экземплярами App и соответствующими SMTP-серверами. Фикстуре app не нужно знать о параметризации smtp_connection, потому что pytest полностью анализирует граф зависимостей фикстур.
Обратите внимание, что фикстура app имеет область действия module и использует фикстуру smtp_connection с областью действия модуля. Пример работал бы и в том случае, если бы smtp_connection кэшировалась в области действия session: фикстуры могут использовать фикстуры с более широкой областью действия, но не наоборот. Например, фикстура с областью действия на всю сессию не может осмысленно использовать фикстуру с областью действия модуля.
Автоматическая группировка тестов по экземплярам фикстур
Во время запуска тестов pytest сводит к минимуму число активных фикстур. Если у вас есть параметризованная фикстура, все использующие её тесты сначала выполнятся с одним экземпляром, а затем, прежде чем будет создан следующий экземпляр фикстуры, вызовутся финализаторы. Помимо прочего, это упрощает тестирование приложений, которые создают и используют глобальное состояние.
В следующем примере используются две параметризованные фикстуры, одна из которых имеет область действия на уровне модуля. Все функции выполняют вызовы print, показывающие последовательность настройки и завершения работы:
# content of test_module.py
import pytest
@pytest.fixture(scope="module", params=["mod1", "mod2"])
def modarg(request):
param = request.param
print(" SETUP modarg", param)
yield param
print(" TEARDOWN modarg", param)
@pytest.fixture(scope="function", params=[1, 2])
def otherarg(request):
param = request.param
print(" SETUP otherarg", param)
yield param
print(" TEARDOWN otherarg", param)
def test_0(otherarg):
print(" RUN test0 with otherarg", otherarg)
def test_1(modarg):
print(" RUN test1 with modarg", modarg)
def test_2(otherarg, modarg):
print(f" RUN test2 with otherarg {otherarg} and modarg {modarg}")
Запустим тесты в подробном режиме и выведем результаты вызовов print:
$ pytest -v -s test_module.py =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-9.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache rootdir: /home/sweet/project collecting ... collected 8 items test_module.py::test_0[1] SETUP otherarg 1 RUN test0 with otherarg 1 PASSED TEARDOWN otherarg 1 test_module.py::test_0[2] SETUP otherarg 2 RUN test0 with otherarg 2 PASSED TEARDOWN otherarg 2 test_module.py::test_1[mod1] SETUP modarg mod1 RUN test1 with modarg mod1 PASSED test_module.py::test_2[mod1-1] SETUP otherarg 1 RUN test2 with otherarg 1 and modarg mod1 PASSED TEARDOWN otherarg 1 test_module.py::test_2[mod1-2] SETUP otherarg 2 RUN test2 with otherarg 2 and modarg mod1 PASSED TEARDOWN otherarg 2 test_module.py::test_1[mod2] TEARDOWN modarg mod1 SETUP modarg mod2 RUN test1 with modarg mod2 PASSED test_module.py::test_2[mod2-1] SETUP otherarg 1 RUN test2 with otherarg 1 and modarg mod2 PASSED TEARDOWN otherarg 1 test_module.py::test_2[mod2-2] SETUP otherarg 2 RUN test2 with otherarg 2 and modarg mod2 PASSED TEARDOWN otherarg 2 TEARDOWN modarg mod2 ============================ 8 passed in 0.12s =============================
Видно, что параметризованный ресурс modarg с областью действия модуля определил порядок выполнения тестов таким образом, чтобы свести число «активных» ресурсов к минимуму. Финализатор параметризованного ресурса mod1 был выполнен до настройки ресурса mod2.
В частности, обратите внимание: test_0 полностью независим и завершается первым. Затем test_1 выполняется с mod1, test_2 — с mod1, после чего test_1 выполняется с mod2, а test_2 — с mod2.
Параметризованный ресурс otherarg (с областью действия функции) настраивался перед каждым тестом, который его использовал, и завершал работу после него.
Использование фикстур в классах и модулях с usefixtures
Иногда тестовым функциям не требуется напрямую обращаться к объекту фикстуры. Например, тестам может понадобиться работать с пустым каталогом в качестве текущего рабочего каталога, но при этом конкретный каталог не имеет значения. Вот как можно использовать стандартные фикстуры tempfile и pytest для этого. Создание фикстуры мы выносим в файл conftest.py:
# content of conftest.py
import os
import tempfile
import pytest
@pytest.fixture
def cleandir():
with tempfile.TemporaryDirectory() as newpath:
old_cwd = os.getcwd()
os.chdir(newpath)
yield
os.chdir(old_cwd)
а её использование объявляем в тестовом модуле с помощью маркера usefixtures:
# content of test_setenv.py
import os
import pytest
@pytest.mark.usefixtures("cleandir")
class TestDirectoryInit:
def test_cwd_starts_empty(self):
assert os.listdir(os.getcwd()) == []
with open("myfile", "w", encoding="utf-8") as f:
f.write("hello")
def test_cwd_again_starts_empty(self):
assert os.listdir(os.getcwd()) == []
Благодаря маркеру usefixtures фикстура cleandir будет необходима для выполнения каждого тестового метода, как если бы вы указали аргумент функции «cleandir» для каждого из них. Запустим тесты, чтобы убедиться, что фикстура активируется и тесты проходят:
$ pytest -q .. [100%] 2 passed in 0.12s
Можно указать несколько фикстур таким образом:
@pytest.mark.usefixtures("cleandir", "anotherfixture")
def test(): ...
Кроме того, можно задать использование фикстур на уровне тестового модуля с помощью pytestmark:
pytestmark = pytest.mark.usefixtures("cleandir")
Фикстуры, необходимые всем тестам в проекте, также можно поместить в файл конфигурации:
# content of pytest.toml [pytest] usefixtures = ["cleandir"]
Предупреждение
@pytest.mark.usefixtures нельзя использовать с функциями фикстур. Например, так делать нельзя:
@pytest.mark.usefixtures("my_other_fixture")
@pytest.fixture
def my_fixture_that_sadly_wont_use_my_other_fixture(): ...
Переопределение фикстур на разных уровнях
В относительно большом наборе тестов может понадобиться переопределить фикстуру, чтобы дополнить или изменить её поведение в определённых тестовых модулях или каталогах.
Переопределение фикстуры на уровне каталога (conftest)
Предположим, структура файлов тестов выглядит так:
tests/
conftest.py
# content of tests/conftest.py
import pytest
@pytest.fixture
def username():
return 'username'
test_something.py
# content of tests/test_something.py
def test_username(username):
assert username == 'username'
subdir/
conftest.py
# content of tests/subdir/conftest.py
import pytest
@pytest.fixture
def username(username):
return 'overridden-' + username
test_something_else.py
# content of tests/subdir/test_something_else.py
def test_username(username):
assert username == 'overridden-username'
Как видите, фикстуру с тем же именем можно переопределить на уровне определённого тестового каталога. Обратите внимание, что к фикстуре base или super можно легко обратиться из фикстуры overriding — как показано в примере выше.
Переопределение фикстуры на уровне тестового модуля
Предположим, структура файлов тестов выглядит так:
tests/
conftest.py
# content of tests/conftest.py
import pytest
@pytest.fixture
def username():
return 'username'
test_something.py
# content of tests/test_something.py
import pytest
@pytest.fixture
def username(username):
return 'overridden-' + username
def test_username(username):
assert username == 'overridden-username'
test_something_else.py
# content of tests/test_something_else.py
import pytest
@pytest.fixture
def username(username):
return 'overridden-else-' + username
def test_username(username):
assert username == 'overridden-else-username'
В приведённом выше примере фикстуру с тем же именем можно переопределить для определённого тестового модуля.
Переопределение фикстуры с помощью прямой параметризации теста
Предположим, структура файлов тестов выглядит так:
tests/
conftest.py
# content of tests/conftest.py
import pytest
@pytest.fixture
def username():
return 'username'
@pytest.fixture
def other_username(username):
return 'other-' + username
test_something.py
# content of tests/test_something.py
import pytest
@pytest.mark.parametrize('username', ['directly-overridden-username'])
def test_username(username):
assert username == 'directly-overridden-username'
@pytest.mark.parametrize('username', ['directly-overridden-username-other'])
def test_username_other(other_username):
assert other_username == 'other-directly-overridden-username-other'
В приведённом выше примере значение фикстуры переопределяется значением параметра теста. Обратите внимание, что таким образом значение фикстуры можно переопределить, даже если тест не использует её напрямую (не указывает её в сигнатуре функции).
Переопределение параметризованной фикстуры непараметризованной и наоборот
Предположим, структура файлов тестов выглядит так:
tests/
conftest.py
# content of tests/conftest.py
import pytest
@pytest.fixture(params=['one', 'two', 'three'])
def parametrized_username(request):
return request.param
@pytest.fixture
def non_parametrized_username(request):
return 'username'
test_something.py
# content of tests/test_something.py
import pytest
@pytest.fixture
def parametrized_username():
return 'overridden-username'
@pytest.fixture(params=['one', 'two', 'three'])
def non_parametrized_username(request):
return request.param
def test_username(parametrized_username):
assert parametrized_username == 'overridden-username'
def test_parametrized_username(non_parametrized_username):
assert non_parametrized_username in ['one', 'two', 'three']
test_something_else.py
# content of tests/test_something_else.py
def test_username(parametrized_username):
assert parametrized_username in ['one', 'two', 'three']
def test_username(non_parametrized_username):
assert non_parametrized_username == 'username'
В приведённом выше примере параметризованная фикстура переопределяется непараметризованной, а непараметризованная фикстура — параметризованной для определённого тестового модуля. Очевидно, то же относится и к уровню тестового каталога.
Использование фикстур из других проектов
Обычно проекты, поддерживающие pytest, используют точки входа, поэтому достаточно установить такие проекты в окружение, чтобы их фикстуры стали доступны для использования.
Если вы хотите использовать фикстуры из проекта, который не использует точки входа, можно определить pytest_plugins в файле conftest.py верхнего уровня, чтобы зарегистрировать этот модуль как плагин.
Предположим, что в mylibrary.fixtures есть несколько фикстур и вы хотите повторно использовать их в каталоге app/tests.
Для этого достаточно определить pytest_plugins в app/tests/conftest.py, указав этот модуль.
pytest_plugins = "mylibrary.fixtures"
Таким образом mylibrary.fixtures регистрируется как плагин, и все его фикстуры и хуки становятся доступны тестам в app/tests.
Примечание
Иногда пользователи импортируют фикстуры из других проектов, чтобы использовать их, однако такой подход не рекомендуется: импорт фикстур в модуль приводит к тому, что pytest регистрирует их как определённые в этом модуле.
Это может иметь незначительные последствия, например фикстуры могут отображаться несколько раз в pytest --help, но такой подход не рекомендуется, поскольку в будущих версиях это поведение может измениться или перестать работать.
© 2015–2026 Holger Krekel and pytest-dev team
Licensed under the MIT License.
https://docs.pytest.org/en/stable/how-to/fixtures.html