PyTest Fixture
| Введение | |
| Общая переменная | |
| Изменить общую переменную | |
| function | |
| Порядок тестов | |
| Перезапуск тестов | |
| Suite setup и teardown | |
| Похожие статьи |
Введение
В этой статье вы можете познакомиться с фикстурами PyTest.
Официальная документация
Общая переменная
С помощью fixture можно обобществить какую-то перменную между тестами в модуле.
Для этого нужно указать тип фикстуры module
import random def generate_word(): route_name "" symbols ["A", "E", "H", "K", "L", "M", "N", "T", "4", "7", "8"] for _ in range(5): route_name += random.choice(symbols) return route_name
import pytest import json from utils import generate_word @pytest.fixture(scope="module") def shared_word(): return generate_word() def test_save_word(shared_word): data = {"value": shared_word} with open("output.json", "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) def test_word_is_saved(shared_word): with open("output.json", "r", encoding="utf-8") as f: data = json.load(f) assert data["value"] == shared_word
python -m pytest -vv test_fixt.py
============================= test session starts ============================= collecting ... collected 2 items fixtures/test_fixt.py::test_save_word PASSED [ 50%] fixtures/test_fixt.py::test_word_is_saved PASSED [100%] ============================== 2 passed in 0.02s ============================== Process finished with exit code 0
Изменить общую переменную
Часто бывает нужно не просто прочитать одно и то же значение в разных тестах, но и изменить его.
Типичный случай - последовательные тесты, которых не следует бояться там, где они действительно нужны.
Фикстура должна будет возвращать какой-то
изменямый (mutable) объект
. Обычно это
словарь.
import pytest @pytest.fixture(scope="module") def shared(): return {} def test_one(shared): shared["value"] = 1 assert isinstance(shared["value"], int) def test_two(shared): assert 2 == shared["value"] + 1 def test_three(shared): print("\n", shared) assert isinstance(shared, dict)
============================= test session starts ============================= collecting ... collected 3 items fixtures/shared.py::test_one PASSED [ 33%] fixtures/shared.py::test_two PASSED [ 66%] fixtures/shared.py::test_three PASSED [100%] {'value': 1} ============================== 3 passed in 0.01s ==============================
Про функцию isinstance() вы можете прочитать здесь .
Тот же самый результат можно получить и с помощью
списка
.
Заменим возвращаемый фикстурой объект на пустой список. Для разнообразия изменим список и во втором тесте тоже.
import pytest @pytest.fixture(scope="module") def shared(): return [] def test_one(shared): shared.append(1) assert isinstance(shared[0], int) def test_two(shared): shared[0] = shared[0] + 1 shared.append(8) assert 2, 8 == (shared[0], shared[1]) def test_three(shared): print("\n", shared) assert isinstance(shared, list)
============================= test session starts ============================= collecting ... collected 3 items fixtures/shared.py::test_one PASSED [ 33%] fixtures/shared.py::test_two PASSED [ 66%] fixtures/shared.py::test_three PASSED [100%] [2, 8] ============================== 3 passed in 0.01s ==============================
function
Чтобы фикстура использовалась каждым тестом, нужно задать уровень function
В следующем примере мы будем выводить на экран текст перед каждый тестом. Для этого нужно использовать флаг -s.
import pytest @pytest.fixture(scope="function") def announce_new_test(): print("\n\nINFO: TestSetup.ru ---- new test case ----\n") def test_one(announce_new_test): assert 1 == 1 def test_two(announce_new_test): assert 2 == 2
python -m pytest -sv test_fixt.py
====================== test session starts ====================== platform win32 -- Python 3.12.10, pytest-9.0.1, pluggy-1.6.0 -- C:\A\pytest\venv\Scripts\python.exe cachedir: .pytest_cache rootdir: C:\A\pytest plugins: order-1.3.0, rerunfailures-16.1 collected 2 items fixtures/f.py::test_one INFO: TestSetup.ru ---- new test case ---- PASSED fixtures/f.py::test_two INFO: TestSetup.ru ---- new test case ---- PASSED ======================= 2 passed in 0.01s =======================
При таком выводе потерялись проценты [ 50%] и [100%].
PyCharm
умеет выводить и проценты и stdout. Я пока не научился. Если знаете как это сделать - напишите в телеграм.
Изменим код так, чтобы фикстура использовалась автоматически и выводила на экран имя каждого теста в начале и в конце.
import pytest @pytest.fixture(autouse=True) def announce_new_test(request): print(f"\n\nTEST: {request.node.name}\n{request.node.nodeid}\n") yield print(f"\nEND OF: {request.node.name} {request.node.nodeid}\n\n") def test_one(): assert 1 == 1 def test_two(): assert 2 == 2
============================= test session starts ============================= … collected 2 items fixtures/test_fixt.py::test_one TEST: test_one fixtures/test_fixt.py::test_one PASSED END OF: test_one fixtures/test_fixt.py::test_one fixtures/test_fixt.py::test_two TEST: test_two fixtures/test_fixt.py::test_two PASSED END OF: test_two fixtures/test_fixt.py::test_two ============================== 2 passed in 0.01s ==============================
Порядок тестов
Чтобы управлять порядком выполнения тестов нужно установить библиотеку pytest-order
python -m pip install pytest-order
Collecting pytest-order Downloading pytest_order-1.3.0-py3-none-any.whl.metadata (7.7 kB) Requirement already satisfied: pytest>= 6.2.4 in c:\a\pytest\venv\lib\site-packages (from pytest-order) (9.0.1) Requirement already satisfied: colorama>= 0.4 in c:\a\pytest\venv\lib\site-packages (from pytest>= 6.2.4->pytest-order) (0.4.6) Requirement already satisfied: iniconfig>= 1.0.1 in c:\a\pytest\venv\lib\site-packages (from pytest>= 6.2.4->pytest-order) (2.3.0) Requirement already satisfied: packaging>= 22 in c:\a\pytest\venv\lib\site-packages (from pytest>= 6.2.4->pytest-order) (25.0) Requirement already satisfied: pluggy<2,>= 1.5 in c:\a\pytest\venv\lib\site-packages (from pytest>= 6.2.4->pytest-order) (1.6.0) Requirement already satisfied: pygments>= 2.7.2 in c:\a\pytest\venv\lib\site-packages (from pytest>= 6.2.4->pytest-order) (2.19.2) Downloading pytest_order-1.3.0-py3-none-any.whl (14 kB) Installing collected packages: pytest-order Successfully installed pytest-order-1.3.0
С помощью @pytest.mark.order() можно явно указать порядок выполнения теста.
Напишем два теста и сделаем так, чтобы второй (нижний) выполнялся раньше первого (верхнего по коду).
import pytest @pytest.mark.order(2) def test_one(): assert 1 == 1 @pytest.mark.order(1) def test_two(): assert 2 == 2
python -m pytest -vv test_order.py
================================= test session starts ================================= platform win32 -- Python 3.12.10, pytest-9.0.1, pluggy-1.6.0 -- C:\A\pytest\venv\Scripts\python.exe cachedir: .pytest_cache rootdir: C:\A\pytest plugins: order-1.3.0 collected 2 items fixtures/test_order.py::test_two PASSED [ 50%] fixtures/test_order.py::test_one PASSED [100%] ================================== 2 passed in 0.01s ==================================
Перезапуск тестов
Для перезапуска упавших тестов нужно установить библиотеку pytest-rerunfailures
python -m pip install pytest-rerunfailures
Collecting pytest-rerunfailures Downloading pytest_rerunfailures-16.1-py3-none-any.whl.metadata (21 kB) Requirement already satisfied: packaging>=17.1 in c:\autotest\sandbox\python\pytest\venv\lib\site-packages (from pytest-rerunfailures) (25.0) Requirement already satisfied: pytest!=8.2.2,>=7.4 in c:\autotest\sandbox\python\pytest\venv\lib\site-packages (from pytest-rerunfailures) (9.0.1) Requirement already satisfied: colorama>=0.4 in c:\autotest\sandbox\python\pytest\venv\lib\site-packages (from pytest!=8.2.2,>=7.4->pytest-rerunfailures) (0.4.6) Requirement already satisfied: iniconfig>=1.0.1 in c:\autotest\sandbox\python\pytest\venv\lib\site-packages (from pytest!=8.2.2,>=7.4->pytest-rerunfailures) (2.3.0) Requirement already satisfied: pluggy<2,>=1.5 in c:\autotest\sandbox\python\pytest\venv\lib\site-packages (from pytest!=8.2.2,>=7.4->pytest-rerunfailures) (1.6.0) Requirement already satisfied: pygments>=2.7.2 in c:\autotest\sandbox\python\pytest\venv\lib\site-packages (from pytest!=8.2.2,>=7.4->pytest-rerunfailures) (2.19.2) Downloading pytest_rerunfailures-16.1-py3-none-any.whl (14 kB) Installing collected packages: pytest-rerunfailures
С помощью @pytest.mark.flaky можно указать сколько раз и с каким интервалом перезапускать тест.
Предположим, мы тестируем приложение, которое медленно загружается.
Допустим, что установленный предел ожидания 12 секунд, и нам нужно убедиться, что за это время приложение загрузилось.
Ожидать 12 секунд каждый раз мы не хотим. Приложение может загрузиться за 6 или даже за 3 секунды.
Попробуем проверить через 3 секунды. При первом обращении оно может быть загрузилось, а может быть нет.
Если оно не загрузилось - это ещё не означает что найден баг. Подождем ещё 3 секунды, потом ещё и так до 12.
Для симуляции такого приложения я взял функцию из статьи «Счётчик вызовов»
# apps.py def slow_app(): slow_app.calls += 1 loaded = 0 if slow_app.calls > 3: loaded = 1 return loaded slow_app.calls = 0
Тест будет выглядеть следующим оригинальным образом
import pytest from apps import slow_app @pytest.mark.flaky(reruns=4, reruns_delay=3) def test_app_is_loaded(): loaded = slow_app() assert loaded == 1
=========================== test session starts ============================ platform win32 -- Python 3.12.10, pytest-9.0.1, pluggy-1.6.0 -- C:\A\pytest\venv\Scripts\python.exe cachedir: .pytest_cache rootdir: C:\A\pytest plugins: order-1.3.0, rerunfailures-16.1 collected 1 item fixtures/test_flaky.py::test_app_is_loaded RERUN [100%] fixtures/test_flaky.py::test_app_is_loaded RERUN [100%] fixtures/test_flaky.py::test_app_is_loaded RERUN [100%] fixtures/test_flaky.py::test_app_is_loaded PASSED [100%] ======================== 1 passed, 3 rerun in 9.05s ========================
Из логов видно, что наше приложение-заглушка «загрузилось» за 9 секуд, что укладывается в 12, поэтому тест прошёл хоть и с тремя перезапусками.
Suite setup и teardown
Одно из типичных применений фикстур, не только в PyTest но и в других фреймворках - это
подготовка и сворачивание окружения.
Аналогом того, что в
Robot Framework
выполняют Suite Setup и Suite Teardown будет одна фикстура, раздлённая на две части
с помощью yield.
Пример такой фикстуры, которая импортирует функции start_app() и close_app() и использует их в начале и конце тестовой сессии.
from tests.suite_setup import start_app from tests.suite_teardown import close_app @pytest.fixture(scope="session", autouse=True) def suite_setup_teardown(): start_app() yield close_app()
Автор статьи: Андрей Олегович
| PyTest | |
| Основы | |
| Параметризация | |
| conftest.py | |
| Удалённый запуск | |
| Ошибки | |
| Видео |