Фикстуры используются в качестве подготовки контекста для выполнения тестов. Например, выполнение соединения с базой данных и последующей передачей объекта соединения в тестовую функцию, создания в базе данных временных записей или пользователей, создания временных директорий или файлов для использования определенным тестом и т.д. И последующей очисткой этих временных записей, файлов, директорий или закрытия соединения с базой данных.
В материале на примере разбирается, как можно создать контекст соединения в фикстуре, передать эту фикстуру тестовой функции для проведения тестов, а после их прохождения закрыть открытое соединение.
Имя функции-фикстуры можно передавать в качестве аргумента другой функции (тесту или фикстуре). Для каждого такого аргумента функция-фикстура предоставляет свой объект. Для того, чтобы простая функция стала фикстурой, необходимо обернуть ее декоратором @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()
оказал на этот объект.