pytest: фикстуры, параметризация и продвинутые тесты
В предыдущей статье мы познакомились с основами pytest: научились писать и запускать простые тесты. Теперь пришло время углубиться в две самые мощные и часто используемые концепции pytest, которые делают его таким гибким и удобным: фикстуры и параметризация.
Фикстуры: подготовка и управление тестовым окружением ⚙️
Часто для проведения теста необходимо выполнить некоторые предварительные действия: подготовить данные, настроить тестовый объект, установить соединение с базой данных и т.д. После теста может потребоваться выполнить очистку: удалить временные файлы, закрыть соединения.
Фикстуры в pytest — это функции, которые служат для настройки и предоставления данных или объектов, необходимых для ваших тестов. Они помогают сделать тесты более чистыми, структурированными и избежать дублирования кода.
Создание простой фикстуры
Фикстура определяется с помощью декоратора @pytest.fixture.
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 в фикстуре.
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: Фикстура выполняется один раз за всю тестовую сессию.
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 получает её через аргумент — без импорта.
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.
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.
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 тестовому случаю, что улучшает читаемость вывода при большом количестве параметров.
Комбинирование фикстур и параметризации
Фикстуры и параметризация могут использоваться вместе для создания очень гибких тестовых сценариев.
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 является верным?
