Как использовать приспособления

См.также

О светильниках

См.также

Fixtures reference

«Требующие» приспособления

На базовом уровне тестовые функции запрашивают необходимые им приспособления, объявляя их в качестве аргументов.

Когда 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 обнаружит и вызовет функцию фикстуры @pytest.fixture, помеченную smtp_connection. Запуск теста выглядит следующим образом:

$ pytest test_module.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.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 предлагают очень полезную систему разрыва, которая позволяет нам определить конкретные шаги, необходимые для того, чтобы каждый фикстур убрал за собой.

Эта система может быть использована двумя способами.

2. Добавление финализаторов напрямую

Хотя фикстуры выхода считаются более чистым и простым вариантом, есть и другой выбор, а именно добавление функций «финализатора» непосредственно в объект request-context теста. Это дает такой же результат, как и фикстуры выхода, но требует немного больше многословия.

Чтобы использовать этот подход, мы должны запросить объект request-context (точно так же, как мы запросили бы другой фикшн) в фикшне, для которого нам нужно добавить код разрушения, а затем передать вызываемый объект, содержащий этот код разрушения, в его метод 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

Он немного длиннее, чем приспособления для урожая, и немного сложнее, но он предлагает некоторые нюансы для тех случаев, когда вы находитесь в затруднительном положении.

$ pytest -q test_emaillib.py
.                                                                    [100%]
1 passed in 0.12s

Примечание по поводу порядка финализации

Финализаторы выполняются в порядке «первый по порядку». Для фикстур с выходом первым выполняется код разрыва из самой правой фикстуры, т.е. последнего тестового параметра.

# 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-7.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-7.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

Эта версия намного компактнее, но ее также труднее читать, у нее не очень описательные названия приспособлений, и ни одно из приспособлений не может быть легко использовано повторно.

Есть и более серьезная проблема, которая заключается в том, что если любой из этих шагов в настройке вызовет исключение, ни один из кодов разрушения не будет запущен.

Одним из вариантов может быть использование метода addfinalizer вместо фиксаторов yield, но это может стать довольно сложным и трудным для поддержки (и это уже не будет компактным).

$ pytest -q test_emaillib.py
.                                                                    [100%]
1 passed in 0.12s

Безопасная конструкция крепления

Самая безопасная и простая структура приспособлений требует ограничить приспособления только одним действием, изменяющим состояние каждого, а затем объединить их вместе с кодом разрыва, как было показано the email examples above.

Вероятность того, что операция, изменяющая состояние, может завершиться неудачей, но при этом изменить состояние, ничтожно мала, поскольку большинство таких операций обычно основаны на transaction (по крайней мере, на том уровне тестирования, где состояние может быть оставлено). Поэтому, если мы убедимся, что любое успешное действие по изменению состояния будет уничтожено путем перемещения его в отдельную функцию фиксации и отделения от других, потенциально неудачных действий по изменению состояния, то наши тесты будут иметь наилучшие шансы оставить тестовую среду такой, какой они ее нашли.

Для примера, допустим, у нас есть сайт со страницей входа, и у нас есть доступ к API администратора, где мы можем генерировать пользователей. Для нашего теста мы хотим:

  1. Создайте пользователя через этот API администратора

  2. Запуск браузера с помощью Selenium

  3. Перейдите на страницу входа на наш сайт

  4. Войдите в систему как пользователь, которого мы создали

  5. Убедитесь, что их имя указано в заголовке целевой страницы

Мы не хотели бы оставлять этого пользователя в системе, как не хотели бы оставлять запущенную сессию браузера, поэтому мы хотим убедиться, что фиксы, которые создают эти вещи, убирают за собой.

Вот как это может выглядеть:

Примечание

В данном примере подразумевается, что некоторые фиксаторы (например, 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. Но это нормально, потому что это атомарные операции, и поэтому не имеет значения, какая из них выполняется первой, потому что последовательность событий для теста все равно linearizable. Но что имеет значение, так это то, что независимо от того, какая из них выполняется первой, если одна из них вызовет исключение, а другая не вызовет, то ни одна из них ничего не оставит после себя. Если driver выполнится раньше user, а user вызовет исключение, драйвер все равно выйдет, а пользователь так и не будет создан. А если бы исключение было вызвано командой driver, то драйвер никогда не был бы запущен, и пользователь никогда не был бы создан.

Безопасное выполнение нескольких операторов assert

Иногда вы можете захотеть запустить несколько утверждений после выполнения всех этих настроек, что вполне логично, поскольку в более сложных системах одно действие может вызвать несколько вариантов поведения. pytest имеет удобный способ обработки этого, и он объединяет в себе все то, что мы рассмотрели до сих пор.

Все, что нужно, это перейти к более широкому диапазону, затем определить шаг act как приспособление autouse, и, наконец, убедиться, что все приспособления нацелены на этот диапазон более высокого уровня.

Давайте возьмем an example from above и немного подправим его. Допустим, помимо проверки наличия приветственного сообщения в заголовке, мы также хотим проверить наличие кнопки выхода и ссылки на профиль пользователя.

Давайте посмотрим, как мы можем структурировать это, чтобы можно было запускать несколько утверждений без необходимости повторять все эти шаги снова.

Примечание

В данном примере подразумевается, что некоторые фиксаторы (например, 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)
    def login(self, 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.

Каждый метод должен запрашивать только те фикстуры, которые ему действительно нужны, не заботясь о порядке. Это происходит потому, что фикстура act является фикстурой autouse, и она позаботилась о том, чтобы все остальные фикстуры выполнялись до нее. Больше нет необходимости изменять состояние, поэтому тесты могут делать столько запросов, не изменяющих состояние, сколько захотят, не рискуя наступить на пятки другим тестам.

Фикция login также определяется внутри класса, поскольку не все другие тесты в модуле будут ожидать успешного входа в систему, и act может потребоваться обрабатывать немного по-другому для другого тестового класса. Например, если бы мы хотели написать другой тестовый сценарий, связанный с отправкой плохих учетных данных, мы могли бы обработать его, добавив в тестовый файл что-то вроде этого:

class TestLandingPageBadCredentials:
    @pytest.fixture(scope="class")
    def faux_user(self, 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 может быть либо списком строк для использования, либо функцией, которая будет вызвана со значением fix и затем должна вернуть строку для использования. В последнем случае, если функция возвращает None, то будет использован автоматически сгенерированный ID pytest.

В результате выполнения описанных выше тестов используются следующие идентификаторы тестов:

$ pytest --collect-only
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 12 items

<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-7.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-7.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}")

Давайте запустим тесты в режиме verbose и посмотрим на результаты печати:

$ pytest -v -s test_module.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.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.

В частности, обратите внимание, что тест_0 является полностью независимым и завершается первым. Затем выполняется тест_1 с mod1, затем тест_2 с mod1, затем тест_1 с mod2 и, наконец, тест_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") 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")

Также можно поместить фикстуры, необходимые для всех тестов в вашем проекте, в ini-файл:

# content of pytest.ini
[pytest]
usefixtures = cleandir

Предупреждение

Обратите внимание, что эта метка не действует в функциях приспособления. Например, это не будет работать, как ожидалось:

@pytest.mark.usefixtures("my_other_fixture")
@pytest.fixture
def my_fixture_that_sadly_wont_use_my_other_fixture():
    ...

В настоящее время это не вызывает никаких ошибок или предупреждений, но предполагается, что это будет обрабатываться issue #3664.

Переопределение приспособлений на различных уровнях

В относительно большом тестовом наборе вам, скорее всего, понадобится override global или root фикстура с locally определенной, сохраняя читаемость и сопровождаемость тестового кода.

Отмена фиксации на уровне папки (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'

    subfolder/
        conftest.py
            # content of tests/subfolder/conftest.py
            import pytest

            @pytest.fixture
            def username(username):
                return 'overridden-' + username

        test_something_else.py
            # content of tests/subfolder/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, используют entry points, поэтому простая установка этих проектов в окружение сделает эти фикстуры доступными для использования.

Если вы хотите использовать фикстуры из проекта, который не использует точки входа, вы можете определить 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, но это не рекомендуется, потому что такое поведение может измениться/перестать работать в будущих версиях.

Вернуться на верх