Conftest.py в Pytest: мощный инструмент для организации тестов

Пройдите тест, узнайте какой профессии подходите
Сколько вам лет
0%
До 18
От 18 до 24
От 25 до 34
От 35 до 44
От 45 до 49
От 50 до 54
Больше 55

Для кого эта статья:

  • Python-разработчики, интересующиеся автоматизированным тестированием
  • QA-инженеры, стремящиеся улучшить организацию тестов
  • Студенты и начинающие тестировщики, желающие освоить технологии pytest и conftest.py

    Когда тестирование становится интегральной частью разработки, организация тестов превращается в критическое искусство. Файл conftest.py — это тот невоспетый герой pytest, который трансформирует хаотический набор тестовых файлов в элегантную, масштабируемую архитектуру. Многие Python-разработчики ошибочно считают его просто хранилищем фикстур, не осознавая полной мощи этого инструмента для централизации настроек, упрощения поддержки и существенного сокращения дублирования кода. 🚀

Погружаетесь в мир автоматизированного тестирования? На Курсе тестировщика ПО от Skypro вы не только освоите pytest и conftest.py, но и получите глубокое понимание всех аспектов тестирования. Наши студенты учатся создавать масштабируемые тестовые фреймворки, которые впечатляют работодателей уже на собеседованиях. Выйдите за рамки базовых знаний и станьте QA-инженером, который меняет качество продуктов к лучшему!

Что такое conftest.py и его роль в экосистеме pytest

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

Ключевая сила conftest.py заключается в его способности создавать иерархическую структуру настроек. Вы можете иметь несколько файлов conftest.py на разных уровнях директорий, формируя многоуровневую конфигурацию, где каждый уровень наследует и может переопределять настройки вышестоящих уровней. 🔄

Алексей Воронов, Lead QA Engineer

Когда я пришёл в проект с более чем 5000 тестов, каждый разработчик использовал свой подход к созданию тестовых окружений. В некоторых файлах фикстуры дублировались десятки раз с минимальными вариациями. Первым делом я централизовал базовые фикстуры в conftest.py на уровне корня проекта — для подключения к тестовой базе данных, аутентификации и основных операций с API.

Затем создал структуру conftest.py для каждого логического модуля с более специфичными фикстурами. Через три недели мы сократили кодовую базу тестов на 30%, повысили скорость выполнения на 25% и, что важнее всего, снизили время на написание новых тестов вдвое. Новички в команде теперь могли быстро разобраться в структуре тестов и начать писать свои с минимальным погружением.

Основные преимущества использования conftest.py:

  • Централизация конфигурации — единая точка для настроек тестирования
  • Уменьшение дублирования — общие фикстуры определяются единожды
  • Модульность — разделение настроек по функциональным областям
  • Автоматический импорт — доступность фикстур без явного импорта
  • Гибкость — иерархическая структура позволяет тонко настраивать окружение
Аспект Без conftest.py С conftest.py
Повторное использование фикстур Требует явного импорта или дублирования Автоматически доступны
Организация кода Часто приводит к "спагетти-коду" Структурированная иерархия
Масштабирование проекта Сложно поддерживать согласованность Естественная эволюция структуры
Настройка окружения Требуется в каждом тестовом модуле Настраивается на нужном уровне один раз

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

Пошаговый план для смены профессии

Создание и структурирование фикстур в conftest.py

Фикстуры — фундамент тестирования в pytest, и conftest.py — идеальное место для их определения. Грамотное структурирование фикстур не только упрощает поддержку, но и значительно повышает читаемость тестов. 📝

Начнем с базового примера создания фикстуры в conftest.py:

Python
Скопировать код
# conftest.py
import pytest
import requests

@pytest.fixture
def api_client():
base_url = "https://api.example.com/v1"
session = requests.Session()
session.headers.update({
"Content-Type": "application/json",
"Authorization": "Bearer test_token"
})
yield session
session.close()

Теперь эта фикстура автоматически доступна во всех тестах директории без необходимости импорта:

Python
Скопировать код
# test_users.py
def test_get_user(api_client):
response = api_client.get("/users/1")
assert response.status_code == 200
assert response.json()["name"] == "Test User"

При структурировании фикстур в больших проектах эффективно использовать композицию — создание фикстур, которые строятся на основе других фикстур:

Python
Скопировать код
# conftest.py
import pytest

@pytest.fixture
def db_connection():
# Настройка соединения с БД
connection = create_db_connection()
yield connection
connection.close()

@pytest.fixture
def user_repository(db_connection):
# Используем существующую фикстуру для создания более специализированной
return UserRepository(db_connection)

@pytest.fixture
def test_user(user_repository):
# Создаём тестового пользователя
user = user_repository.create(name="Test User", email="test@example.com")
yield user
# Очистка после теста
user_repository.delete(user.id)

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

Для организации сложных проектов рекомендую разделять фикстуры по функциональным областям с использованием иерархии файлов conftest.py:

  • project/conftest.py — глобальные фикстуры (настройки логгера, подключения к БД)
  • project/api/conftest.py — фикстуры для тестирования API
  • project/ui/conftest.py — фикстуры для UI-тестов
  • project/unit/conftest.py — фикстуры для модульных тестов

При таком подходе тест получает доступ к фикстурам из текущей директории и всех родительских директорий, что создаёт естественную иерархию наследования.

Дополнительный уровень организации можно добавить с помощью фабрик фикстур — функций, генерирующих параметризированные фикстуры:

Python
Скопировать код
# conftest.py
import pytest

def user_factory(role):
@pytest.fixture
def _user():
# Создаём пользователя с указанной ролью
return {"id": 1, "name": "Test User", "role": role}
return _user

# Создаём конкретные фикстуры из фабрики
admin_user = user_factory("admin")
regular_user = user_factory("user")
guest_user = user_factory("guest")

Область видимости фикстур: от локальной до глобальной

Управление жизненным циклом фикстур через их область видимости (scope) — важнейший аспект оптимизации тестов в pytest. Выбор правильной области видимости существенно влияет как на производительность, так и на изоляцию тестов. 🔍

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

Область видимости Синтаксис Когда создаётся Когда уничтожается Типичное применение
function (по умолчанию) @pytest.fixture Перед каждым тестом После каждого теста Временные данные, специфичные для теста
class @pytest.fixture(scope="class") Перед первым тестом класса После последнего теста класса Ресурсы, общие для класса тестов
module @pytest.fixture(scope="module") Перед первым тестом модуля После последнего теста модуля Соединения с БД, конфигурация модуля
package @pytest.fixture(scope="package") Перед первым тестом пакета После последнего теста пакета Ресурсы, общие для нескольких модулей
session @pytest.fixture(scope="session") Перед первым тестом сессии После последнего теста сессии Глобальные ресурсы, конфигурация среды

Рассмотрим примеры использования разных областей видимости в conftest.py:

Python
Скопировать код
# conftest.py
import pytest
from selenium import webdriver

@pytest.fixture(scope="function")
def temp_file():
# Создаётся для каждого теста
path = "temp_test_data.txt"
with open(path, "w") as f:
f.write("Test data")
yield path
import os
os.remove(path) # Очистка после каждого теста

@pytest.fixture(scope="module")
def db_connection():
# Создаётся один раз на модуль
print("Opening DB connection")
conn = create_connection()
yield conn
print("Closing DB connection")
conn.close()

@pytest.fixture(scope="session")
def web_driver():
# Создаётся один раз на всю сессию тестирования
print("Starting WebDriver")
driver = webdriver.Chrome()
driver.implicitly_wait(10)
yield driver
print("Shutting down WebDriver")
driver.quit()

При выборе области видимости следует руководствоваться несколькими принципами:

  • Производительность — фикстуры с широкой областью видимости сокращают накладные расходы на настройку
  • Изоляция тестов — чем уже область видимости, тем лучше изоляция между тестами
  • Побочные эффекты — широкая область видимости увеличивает риск взаимовлияния тестов
  • Сложность отладки — проблемы в фикстурах сессионного уровня могут влиять на многие тесты

Мария Соколова, Senior Test Automation Engineer

В проекте финтех-компании мы столкнулись с проблемой: тесты API выполнялись почти час из-за постоянного пересоздания тестовых данных. Каждый тест создавал пользователя, аккаунт, добавлял транзакции — а у нас было более 500 тестов.

Я проанализировала зависимости и реорганизовала фикстуры в conftest.py по уровням. Базовые данные (пользователи, аккаунты) мы вынесли в фикстуры с областью session. Для групп связанных тестов создали фикстуры module и class. На уровне function оставили только те данные, которые реально менялись в процессе тестов.

Результат превзошёл ожидания: время прогона сократилось до 8 минут, а стабильность тестов возросла. Ключевым открытием стало то, что правильный выбор области видимости фикстур — это не просто технический нюанс, а стратегическое решение, влияющее на всю тестовую инфраструктуру.

Для сложных сценариев pytest позволяет динамически определять область видимости фикстуры:

Python
Скопировать код
# conftest.py
import pytest
import os

def determine_scope(fixture_name, config):
if os.environ.get("CI") == "true":
# В CI-окружении используем более широкую область 
# для ускорения тестирования
return "module"
# На локальной машине предпочитаем лучшую изоляцию
return "function"

@pytest.fixture(scope=determine_scope)
def dynamic_scoped_fixture():
# Область видимости определяется динамически
return expensive_resource()

Важный нюанс: фикстуры могут зависеть только от фикстур с такой же или более широкой областью видимости. Например, фикстура с областью "session" не может использовать фикстуру с областью "function". 🔄

Настройка тестового окружения через хуки pytest

Хуки pytest — это мощный механизм кастомизации процесса тестирования, и conftest.py — идеальное место для их определения. Хуки позволяют перехватывать и модифицировать различные этапы выполнения тестов, от сбора тестов до формирования отчётов. 🧩

Рассмотрим ключевые хуки, которые можно определить в conftest.py для тонкой настройки тестового окружения:

Python
Скопировать код
# conftest.py
import pytest
import os
import logging

# Настройка логгирования для всех тестов
def pytest_configure(config):
"""
Вызывается в начале тестовой сессии перед сбором тестов
"""
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s',
handlers=[
logging.FileHandler('test_run.log'),
logging.StreamHandler()
]
)
logging.info("Starting test session")

# Регистрация кастомного маркера
config.addinivalue_line(
"markers", "slow: marks test as slow running"
)

# Пропуск медленных тестов на CI
def pytest_runtest_setup(item):
"""
Вызывается перед каждым тестом
"""
if os.environ.get("CI") == "true" and "slow" in item.keywords:
pytest.skip("Skipping slow test in CI environment")

# Модификация сообщения об ошибке
def pytest_assertion_pass(item, lineno, orig):
"""
Вызывается при успешной проверке assert
"""
logging.info(f"Assertion passed in {item.name}")

def pytest_assertion_fail(item, message):
"""
Вызывается при провале assert
"""
logging.error(f"Assertion failed in {item.name}: {message}")

# Кастомизация сбора тестов
def pytest_collect_file(parent, path):
"""
Вызывается для каждого файла при сборе тестов
"""
if path.basename.startswith("integration_") and path.ext == ".py":
# Можно вернуть специальный коллектор для таких файлов
return parent.session.collect_file(path)

Особенно полезными являются хуки для настройки параметров запуска pytest. Например, можно добавить пользовательские параметры командной строки:

Python
Скопировать код
# conftest.py
def pytest_addoption(parser):
"""
Добавление кастомных параметров командной строки
"""
parser.addoption(
"--env",
choices=["dev", "staging", "prod"],
default="dev",
help="Specify the test environment"
)
parser.addoption(
"--api-version", 
default="v1",
help="API version to test"
)

# Затем доступ к параметрам через фикстуру
@pytest.fixture(scope="session")
def env(request):
return request.config.getoption("--env")

@pytest.fixture(scope="session")
def api_base_url(env):
"""
Фикстура, использующая параметр командной строки
"""
urls = {
"dev": "https://dev-api.example.com",
"staging": "https://staging-api.example.com",
"prod": "https://api.example.com"
}
return urls[env]

Вы можете настраивать отчёты о тестировании с помощью специальных хуков:

Python
Скопировать код
# conftest.py
def pytest_terminal_summary(terminalreporter, exitstatus, config):
"""
Вызывается в конце тестовой сессии для вывода финального отчёта
"""
print("\n=== Custom Test Summary ===")
print(f"Total tests: {terminalreporter.stats.get('passed', 0) + terminalreporter.stats.get('failed', 0)}")
print(f"Passed: {len(terminalreporter.stats.get('passed', []))}")
print(f"Failed: {len(terminalreporter.stats.get('failed', []))}")
print(f"Skipped: {len(terminalreporter.stats.get('skipped', []))}")
print(f"Execution time: {terminalreporter.config.duration:.2f} seconds")

# Можно добавить интеграцию с внешними системами
if exitstatus != 0 and os.environ.get("NOTIFY_FAILURES"):
send_notification("Test failures detected!")

Для крупных проектов ценным инструментом являются хуки для настройки параллельного запуска тестов:

Python
Скопировать код
# conftest.py
def pytest_xdist_node_collection_finished(node, ids):
"""
Вызывается на каждом узле при завершении сбора тестов при использовании pytest-xdist
"""
print(f"Node {node.gateway.id} collected {len(ids)} tests")

def pytest_configure_node(node):
"""
Вызывается для настройки удалённого узла при использовании pytest-xdist
"""
node.workerinput["node_id"] = node.gateway.id
# Можно передать информацию удалённому процессу
if node.gateway.id == "w0":
node.workerinput["tests_type"] = "api"
elif node.gateway.id == "w1":
node.workerinput["tests_type"] = "ui"

Список основных хуков pytest и их назначение:

  • pytest_configure — глобальная настройка перед началом сбора тестов
  • pytest_addoption — добавление параметров командной строки
  • pytestcollectionmodifyitems — модификация списка собранных тестов
  • pytestruntestsetup — выполняется перед каждым тестом
  • pytestruntestteardown — выполняется после каждого теста
  • pytestruntestprotocol — контроль всего протокола выполнения теста
  • pytestfixturesetup — выполняется при установке фикстуры
  • pytestfixturepost_finalizer — выполняется после завершения фикстуры
  • pytestreportteststatus — модификация статуса теста в отчёте
  • pytestterminalsummary — финальный отчёт в консоли

Практические сценарии применения conftest.py в проектах

Теория полезна, но настоящее понимание возможностей conftest.py приходит только при рассмотрении конкретных практических сценариев. Давайте исследуем типичные задачи тестирования и решения через conftest.py. 💼

Сценарий 1: Управление тестовыми данными для разных окружений

Python
Скопировать код
# conftest.py
import pytest
import json
import os

def pytest_addoption(parser):
parser.addoption("--env", default="dev")

@pytest.fixture(scope="session")
def config(request):
# Загружаем конфигурацию в зависимости от окружения
env = request.config.getoption("--env")
config_path = f"configs/{env}_config.json"

with open(config_path) as f:
config = json.load(f)

# Обогащаем конфигурацию динамическими данными
config["test_run_id"] = os.environ.get("TEST_RUN_ID", "local_run")

return config

@pytest.fixture(scope="session")
def api_client(config):
"""Клиент API с настройками для указанного окружения"""
client = APIClient(
base_url=config["api_url"],
timeout=config["timeout"],
headers={"X-Environment": config["env_name"]}
)
return client

@pytest.fixture(scope="function")
def test_data(config):
"""Загрузка тестовых данных для конкретного окружения"""
env = config["env_name"]
with open(f"test_data/{env}_data.json") as f:
return json.load(f)

Сценарий 2: Автоматическая авторизация для различных ролей пользователей

Python
Скопировать код
# conftest.py
import pytest
from auth_service import get_token

# Создаём фабрику авторизованных клиентов
def create_authorized_client(role):
@pytest.fixture(scope="function")
def _authorized_client(api_client):
token = get_token(role)
api_client.headers.update({"Authorization": f"Bearer {token}"})
return api_client
return _authorized_client

# Фикстуры для разных ролей
admin_client = create_authorized_client("admin")
user_client = create_authorized_client("user")
guest_client = create_authorized_client("guest")

# Затем в тестах:
# def test_admin_access(admin_client):
# ...
# def test_user_permissions(user_client):
# ...

Сценарий 3: Интеграция с системой отчётности и мониторинга

Python
Скопировать код
# conftest.py
import pytest
import allure
import datetime
import requests

@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
"""Создаёт расширенный отчёт о прохождении теста"""
outcome = yield
report = outcome.get_result()

# Добавляем дополнительные метаданные к отчету
report.start_time = datetime.datetime.now().isoformat()
report.environment = item.config.getoption("--env", default="dev")

if report.when == "call":
# Только для фазы выполнения теста
allure.attach(
body=str(report.duration),
name="Test Duration",
attachment_type=allure.attachment_type.TEXT
)

@pytest.fixture(scope="function", autouse=True)
def report_test_metrics(request):
"""Автоматически отправляет метрики о тестах в систему мониторинга"""
start_time = datetime.datetime.now()

yield

duration = (datetime.datetime.now() – start_time).total_seconds()
test_name = request.node.name
result = "passed" if not request.node.rep_call.failed else "failed"

# Отправка метрик в внешнюю систему (например, Prometheus, Graphite)
try:
requests.post(
"http://metrics-collector.example.com/submit",
json={
"test_name": test_name,
"duration": duration,
"result": result,
"environment": request.config.getoption("--env", default="dev")
},
timeout=1
)
except Exception as e:
print(f"Failed to send metrics: {e}")

Сценарий 4: Параллелизация тестов с учётом зависимостей

Python
Скопировать код
# conftest.py
def pytest_configure(config):
"""Настраивает метаданные тестов для правильной параллелизации"""
if config.getoption("--xdist-workers") not in [None, "auto"]:
# Регистрируем кастомный атрибут для тестов
config.addinivalue_line("markers", 
"resource(name): mark test as requiring specific resource")

def pytest_xdist_node_collection_finished(node, ids):
"""Распределяет тесты по узлам в зависимости от используемых ресурсов"""
import random

# Получаем общее количество узлов
total_nodes = int(node.config.getoption("--numprocesses"))
node_id = int(node.gateway.id.replace('gw', ''))

# Определяем ресурсы для этого узла
# (например, базы данных, API-эндпоинты и т.д.)
resources_per_node = {
0: ["database_a", "api_user"],
1: ["database_b", "api_product"],
# ...и так далее для каждого узла
}

assigned_resources = resources_per_node.get(node_id, [])

# Для каждого узла фильтруем только тесты, использующие его ресурсы
selected_tests = []
for test_id in ids:
for marker in node.session.items_by_id[test_id].iter_markers(name="resource"):
if marker.args[0] in assigned_resources:
selected_tests.append(test_id)
break
else:
# Если тест не требует особых ресурсов, распределяем случайно
if hash(test_id) % total_nodes == node_id:
selected_tests.append(test_id)

# Оставляем для этого узла только отобранные тесты
node.slaveinput["selected_tests"] = selected_tests

Сценарий использования Подходящие инструменты в conftest.py Дополнительные библиотеки
Тестирование микросервисов Динамические фикстуры, factory_boy интеграция pytest-docker-compose, pytest-kubernetes
Тестирование веб-интерфейсов WebDriver фикстуры, скриншоты при ошибках pytest-selenium, pytest-bdd
Нагрузочное тестирование Хуки для метрик производительности pytest-benchmark, locust
Регрессионное тестирование Маркеры, теги, категоризация тестов pytest-randomly, pytest-rerunfailures
Непрерывная интеграция Условная настройка, динамические параметры pytest-xdist, pytest-cov

Продвинутая техника — использование фабрик тестов через conftest.py. Это позволяет динамически генерировать тесты на основе внешних данных:

Python
Скопировать код
# conftest.py
import pytest
import json

def pytest_generate_tests(metafunc):
"""Динамически генерирует тестовые случаи из файла данных"""
if "api_endpoint" in metafunc.fixturenames:
with open("api_endpoints.json") as f:
endpoints = json.load(f)

# Параметризуем тест для каждого эндпоинта
metafunc.parametrize(
"api_endpoint,expected_status", 
[(e["url"], e["status"]) for e in endpoints]
)

Правильно организованный conftest.py становится центральной нервной системой вашего тестового фреймворка. Он создаёт слой абстракции, отделяющий инфраструктурные аспекты от логики самих тестов. Хорошо спроектированная иерархия файлов conftest.py помогает масштабировать тесты вместе с ростом проекта, обеспечивая не только качество, но и поддерживаемость. Мощь этого инструмента раскрывается полностью, когда вы начинаете мыслить о своих тестах не как о наборе отдельных скриптов, а как о целостной архитектуре, где conftest.py является ключевым строительным блоком.

Загрузка...