pytest: фикстуры, параметризация и продвинутые тесты

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

Фикстуры: подготовка и управление тестовым окружением ⚙️

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

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

Создание простой фикстуры

Фикстура определяется с помощью декоратора @pytest.fixture.

test_fixtures_example.py
1import pytest2 3@pytest.fixture4def sample_list():5print("\n(Фикстура sample_list: создаю список)")6return [1, 2, 3, 4, 5]7 8@pytest.fixture9def sample_dict():10print("\n(Фикстура sample_dict: создаю словарь)")11return {"name": "Alice", "age": 30}12 13def test_list_length(sample_list): # имя фикстуры передаётся как аргумент14print("(Тест test_list_length)")15assert len(sample_list) == 516 17def test_dict_name(sample_dict): # другая фикстура18print("(Тест test_dict_name)")19assert sample_dict["name"] == "Alice"20 21def test_list_and_dict_usage(sample_list, sample_dict):22print("(Тест test_list_and_dict_usage)")23assert len(sample_list) > 024assert "age" in sample_dict25 

Если запустить pytest -v -s (флаг -s нужен, чтобы увидеть выводы print из фикстур и тестов):

  • pytest выполнит каждую фикстуру перед тестом, который ее запрашивает.
  • Результат выполнения фикстуры (то, что она возвращает) передается в тест как аргумент.

Завершение работы фикстуры: yield для setup и teardown

Если вам нужно выполнить какие-то действия после того, как тест завершился (например, закрыть соединение с базой данных или удалить временный файл), используйте yield в фикстуре.

test_fixture_yield.py
1import os2 3import pytest4 5@pytest.fixture6def temp_file_fixture():7file_path = "temp_test_file.txt"8print(f"\n(Фикстура: создаю файл ${file_path})")9with open(file_path, "w") as f:10f.write("Hello, pytest!")11 12    yield file_path  # тест получает это значение13 14    print(f"\n(Фикстура: удаляю файл ${file_path})")15    os.remove(file_path)16 17def test_read_temp_file(temp_file_fixture):18file_path = temp_file_fixture # получаем путь к файлу из фикстуры19print(f"(Тест: читаю файл ${file_path})")20with open(file_path, "r") as f:21content = f.read()22assert content == "Hello, pytest!"23 

Код до yield выполняется перед тестом (setup), а код после yield — после теста (teardown).

Области действия фикстур (scope)

По умолчанию фикстура выполняется для каждой тестовой функции, которая ее запрашивает. Это поведение можно изменить с помощью параметра scope в декораторе @pytest.fixture.

Доступные области действия:

  • function (по умолчанию): Фикстура выполняется один раз для каждой тестовой функции.
  • class: Фикстура выполняется один раз для каждого тестового класса.
  • module: Фикстура выполняется один раз для каждого модуля.
  • package: Фикстура выполняется один раз для каждого пакета (Python 3).
  • session: Фикстура выполняется один раз за всю тестовую сессию.
test_scopes.py
1import pytest2 3@pytest.fixture(scope="session")4def session_db_connection():5print("\n(session_db_connection: подключаюсь к БД — ОДИН РАЗ ЗА СЕССИЮ)")6connection = {"status": "connected"} # имитация соединения7yield connection8print("\n(session_db_connection: закрываю соединение — ОДИН РАЗ ЗА СЕССИЮ)")9 10@pytest.fixture(scope="module")11def module_data_setup(session_db_connection): # Эта фикстура зависит от session_db_connection12assert session_db_connection["status"] == "connected"13print("\n(module_data_setup: настраиваю данные модуля — ОДИН РАЗ ЗА МОДУЛЬ)")14data = {"module_id": 123}15yield data16print("\n(module_data_setup: очищаю данные модуля)")17 18class TestUserOperations:19def test_user_create(self, module_data_setup, session_db_connection):20print("(Тест test_user_create)")21assert session_db_connection["status"] == "connected"22assert module_data_setup["module_id"] == 12323 24    def test_user_view(self, module_data_setup, session_db_connection):25        print("(Тест test_user_view)")26        assert session_db_connection["status"] == "connected"27        assert module_data_setup["module_id"] == 12328 29def test_another_operation(module_data_setup, session_db_connection):30print("(Тест test_another_operation в том же модуле)")31assert session_db_connection["status"] == "connected"32assert module_data_setup["module_id"] == 12333 

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

Общие фикстуры: conftest.py

Если у вас есть фикстуры, которые вы хотите использовать в нескольких тестовых файлах, вы можете определить их в специальном файле с именем conftest.py. pytest автоматически обнаруживает этот файл.

  • Файл conftest.py должен находиться в директории с тестами или в родительской директории.
  • Фикстуры, определенные в conftest.py, становятся доступными для всех тестов в этой директории и ее поддиректориях без необходимости их импортировать.

Пример: фикстура global_app_config живёт в conftest.py, а тест из test_module_one.py получает её через аргумент — без импорта.

test_module_one.py
1def test_api_url(global_app_config): # фикстура из conftest.py доступна здесь2print(f"(Тест test_api_url: использую ${global_app_config['api_url']})")3assert "example.com" in global_app_config["api_url"]4 

Автоматическое использование фикстур (autouse)

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

conftest.py
1import time2 3import pytest4 5@pytest.fixture(autouse=True, scope="session")6def print_start_end_session():7print("\n--- НАЧАЛО ТЕСТОВОЙ СЕССИИ ---")8yield9print("\n--- КОНЕЦ ТЕСТОВОЙ СЕССИИ ---")10 11@pytest.fixture(autouse=True, scope="function")12def track_test_duration():13start_time = time.time()14yield15duration = time.time() - start_time16print(f"(Тест выполнялся: ${duration:.4f} сек.)")17 

Используйте autouse с осторожностью, так как это может сделать зависимости тестов менее явными.

Параметризация: один тест, много запусков 🔄

Часто возникает необходимость проверить одну и ту же логику с разными наборами входных данных и ожидаемых результатов. Вместо написания нескольких почти идентичных тестов pytest предлагает параметризацию с помощью маркера @pytest.mark.parametrize.

test_parametrize_example.py
1import pytest2 3from discount import get_discount4 5@pytest.mark.parametrize(6"age, is_member, expected_discount", # имена аргументов тестовой функции7[8(70, False, 0.15), # тестовый случай 19(30, True, 0.10), # тестовый случай 210(16, False, 0.05), # тестовый случай 311(25, False, 0.0), # тестовый случай 412(65, True, 0.15), # пожилой-участник всё равно получает 15%13pytest.param(10, True, 0.05, id="JuniorMember"),14],15)16def test_get_discount_parametrized(age, is_member, expected_discount):17assert get_discount(age, is_member) == expected_discount18 

В этом примере:

  • Тест test_get_discount_parametrized будет запущен несколько раз, по одному разу для каждой тройки значений в списке.
  • pytest.param(...) можно использовать для присвоения пользовательского ID тестовому случаю, что улучшает читаемость вывода при большом количестве параметров.

Комбинирование фикстур и параметризации

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

test_fixtures_and_parametrize.py
1import pytest2 3@pytest.fixture4def user_profile_factory():5def _create_profile(name, role="viewer"):6return {"username": name, "role": role, "permissions": []}7 8    return _create_profile9 10@pytest.mark.parametrize(11"role_to_assign, expected_permission_count",12[13("admin", 5),14("editor", 3),15("viewer", 1),16],17)18def test_user_permissions_after_role_assignment(19user_profile_factory,20role_to_assign,21expected_permission_count,22):23profile = user_profile_factory(name="testuser")24 25    # Имитируем логику присвоения прав в зависимости от роли26    if role_to_assign == "admin":27        profile["permissions"] = ["read", "write", "delete", "manage_users", "publish"]28    elif role_to_assign == "editor":29        profile["permissions"] = ["read", "write", "publish"]30    elif role_to_assign == "viewer":31        profile["permissions"] = ["read"]32 33    profile["role"] = role_to_assign34 35    assert profile["role"] == role_to_assign36    assert len(profile["permissions"]) == expected_permission_count37 38 

Что дальше?

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

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

Какое утверждение о фикстурах и параметризации в pytest является верным?