Сообщить об ошибке.

Фикстура как аргумент теста, модуль pytest в Python

Передача фикстуры как параметр тестовой функции/метода

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

В материале на примере разбирается, как можно создать контекст соединения в фикстуре, передать эту фикстуру тестовой функции для проведения тестов, а после их прохождения закрыть открытое соединение.

Содержание:


Фикстура как аргумент тестовой функции/метода.

Имя функции-фикстуры можно передавать в качестве аргумента другой функции (тесту или фикстуре). Для каждого такого аргумента функция-фикстура предоставляет свой объект. Для того, чтобы простая функция стала фикстурой, необходимо обернуть ее декоратором @pytest.fixture. Рассмотрим простой тестовый модуль, в которой тестовая функция принимает фикстуру в качестве своего аргумента:

# test.py
import pytest

@pytest.fixture
def smtp_connection():
    import smtplib
    # создаем соединение с `smtp.gmail.com`
    connect = smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
    print("SMTP-соединение открыто")
    # оператор `yield` возвращает SMTP-соединение  
    yield connect
    print("SMTP-соединение закрыто")
    connect.close()

# тест запрашивает имя фикстуры  
# `smtp_connection` в качестве аргумента
def test_ehlo(smtp_connection):
    print("Тест начался")
    # проводим тест с соединением
    response, _ = smtp_connection.ehlo()
    # оцениваем результат
    assert response == 250
    print("Тест завершился")

В коде выше, тестовая функция test_ehlo() принимает значение фикстуры smtp_connection() в качестве аргумента. Запустив тест как $ pytest -v -s test.py, можно увидеть весь вывод функции print(), и что тестовая функция была вызвана с аргументом smtp_connection - объектом smtplib.SMTP(), который был создан фикстурой. После завершения теста фикстура перехватила управление и выполнила завершающие операции.

В данном случае фреймворк pytest использует следующий алгоритм для вызова тестовой функции:

  • pytest находит функцию test_ehlo() по ее префиксу test_. Ей передается аргумент с именем smtp_connection, поэтому pytest ищет и находит функцию с именем smtp_connection, помеченную как фикстура.
  • Фикстура smtp_connection() вызывается для создания объекта-функции.
  • Затем вызывается функция test_ehlo(<объект smtp_connection>) и выполняется тест.
  • После завершения теста (неважно с ошибкой или без) управление переходит снова к фикстуре smtp_connection(), где она выполняет завершающий код, расположенный после оператора yield

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

Синтаксис yield можно использовать с оператором контекста with:

import smtplib
import pytest

@pytest.fixture
def smtp_connection():
    with smtplib.SMTP("smtp.gmail.com", 587, timeout=5) as connect:
        # возвращает значение фикстуры
        yield connect

После выполнения теста соединение connect будет разорвано, т.к. объект connect автоматически закрывается после завершения выполнения оператора with.

Альтернативным способом добиться выполнения завершающего кода является использование метода addfinalizer объекта request-context для регистрации финализатора.

Пример использования метода request.addfinalizer() для разрыва соединения в фикстуре smtp_connection():

import smtplib
import pytest

@pytest.fixture(scope="module")
def smtp_connection(request):
    connect = smtplib.SMTP("smtp.gmail.com", 587, timeout=5)

    def fin():
        print("SMTP-соединение закрыто")
        connect.close()

    request.addfinalizer(fin)
    # возвращаем значение фикстуры
    return connect

Фикстуры могут запрашивать другие фикстуры.

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

Пример, демонстрирующий, как фикстуры могут использовать другие фикстуры:

import pytest

@pytest.fixture
def first_entry():
    return "a"

# фикстура `order()` запрашивает в качестве
# аргумента фикстуру `first_entry()`
@pytest.fixture
def order(first_entry):
    return [first_entry]

def test_string(order):
    order.append("b")
    assert order == ["a", "b"]

Тест/фикстура может одновременно запрашивать несколько фикстур.

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

Пример для демонстрации:

import pytest

@pytest.fixture
def first_entry():
    return "a"

@pytest.fixture
def second_entry():
    return 2

# фикстура `order()` запрашивает 
# фикстуры `first_entry()` и `second_entry()`
@pytest.fixture
def order(first_entry, second_entry):
    return [first_entry, second_entry]

@pytest.fixture
def expected_list():
    return ["a", 2, 3.0]

# тест `test_string()` запрашивает 
# фикстуры `order()` и `expected_list()`
def test_string(order, expected_list):
    order.append(3.0)
    assert order == expected_list

Фикстуры могут быть запрошены более одного раза за тест.

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

Другими словами, фикстуры кешируются! Пример:

import pytest

@pytest.fixture
def first_entry():
    return "a"

@pytest.fixture
def order():
    return []

@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() оказал на этот объект.