Подписывайтесь:

Блог AST-SoftPro

Тестирование Python-приложений: pytest, fixtures и mocking

25.05.2026 10 мин чтения
Тестирование Python-приложений: pytest, fixtures и mocking

Введение в тестирование Python-приложений

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

pytest поддерживает различные подходы к тестированию: от простых unit-тестов до интеграции тестов с внешними сервисами. Эта статья охватывает ключевые аспекты работы с pytest: параметризацию, fixtures, mocking (подмену зависимостей) и тестирование асинхронных функций.

Основы pytest: что нужно знать перед началом

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

  • Тест-функция — функция с именем, начинающимся на test_ (или другой префикс через настройку).

  • Фикстуры (fixtures)

  • Это функции, которые предоставляются автоматически перед запуском тестов и могут использоваться как зависимости.

  • Пример:

def test_user_creation(user_fixture):
    assert user_fixture.name == "Alice"

@pytest.fixture
def user_fixture():
    return User(name="Alice", age=30)
  • Параметризация тестов — возможность запускать один и тот же тест с разными наборами входных данных.

  • Пример:

import pytest

def test_add(a, b, expected):
    assert a + b == expected

pytest.mark.parametrize("a,b,expected", [
    (2, 3, 5),
    (-1, 1, 0),
    (0, 0, 0)
])
def test_add(a, b, expected):
    assert a + b == expected
  • Адаптеры — pytest автоматически распознаёт тесты по именованию и структуре. Также можно использовать декораторы для группировки тестов.

  • Пример:

@pytest.mark.smoke
def test_login():
    # тест быстрого входа
    pass

@pytest.mark.integration
def test_database_connection():
    # интеграционный тест
    pass

Фикстуры: управление зависимостями в тестах

Фикстуры — мощный механизм для создания и предоставления зависимостей между тестами. Они позволяют:

  • Изолировать состояние тестов.

  • Повторно использовать одни и те же объекты (например, базы данных, API-серверы).

  • Управлять жизненным циклом объектов (создание/удаление).

Типичные типы фикстур

Тип Описание
Функциональная фикстура (function) Создается и уничтожается перед каждым тестом. Используется по умолчанию, если тип не указан.
Модульная (module) Один экземпляр на весь модуль (файл). Полезно для создания БД или API-сервера один раз.
Классическая (class, session) Для классовых тестов; сессионные фикстуры — один объект на все тесты в сессии.

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

def test_user_create(user_db_fixture):
    user = User(name="Bob", age=25)
    user.save()
    assert len(User.query.all()) == 1
\@pytest.fixture(scope="module")
def user_db_fixture():
    db = DatabaseMock()
    return DbAdapter(db)

Фикстуры с параметрами и зависимостями

Фикстуры могут зависеть от других фикстур. Это позволяет строить сложные тестовые среды.

  • Пример:
def test_order_creation(order_fixture, user_fixture):
    assert order_fixture.user == user_fixture

@pytest.fixture
def user_fixture():
    return User(name="Alice", email="a@example.com")

@pytest.fixture(depends_on=["user_fixture"])  # pytest >= 7.1
def order_fixture(user_fixture):
    return Order(product="Laptop", user=user_fixture)

Автозагружаемые фикстуры и conftest.py

Фикстуры, определённые в файле с именем conftest.py, доступны во всей директории проекта. Это удобно для настройки общего поведения тестов.

  • Пример: создание глобального tmpdir:
# conftest.py
def test_with_temp_folder(tmp_path):
    file = tmp_path / "data.json"
    file.write_text("{\"test\": true}\")

Mocking: подмена зависимостей в тестах

Иногда тесты зависят от внешних сервисов (базы данных, API, файловая система), которые нельзя или не нужно запускать. В таких случаях используют mock-объекты — поддельные реализации этих зависимостей.

  • pytest поддерживает интеграцию с unittest.mock, что позволяет использовать привычный интерфейс из стандартной библиотеки Python.

Базовое использование mock

Пример: тест, имитирующий HTTP-запрос к внешнему API:

def test_fetch_user(user_api_fixture):
    user = UserApi.fetch("123")
    assert user.name == "Alice"

@pytest.fixture
def user_api_fixture(monkeypatch):
    mock_obj = Mock()
    monkeypatch.setattr(UserApi, "fetch", mock_obj.fetch)
    return mock_obj

Использование monkeypatch для подмены атрибутов и функций

def test_config_load(config_fixture):
    assert config_fixture.get("debug") == True

@pytest.fixture
def config_fixture(monkeypatch):
    mock_conf = Mock()
    monkeypatch.setattr(app.config, "get", lambda key: {\"debug":True}[key])  # упрощённо для примера
    return app.config.get

Подмена асинхронных функций (async def)

В Python также можно мокать async функции:

def test_async_request(api_client_fixture):
    response = api_client_fixture.fetch_user("id")
    assert response.status == 200

@pytest.fixture
def api_client_fixture(monkeypatch):
    async def mock_fetch(id):
        return MockResponse(status=200, data={\"name": "John"})
    monkeypatch.setattr(ApiClient, "fetch_user", mock_fetch)
    return ApiClient

Примечание: monkeypatch доступен только в тестах с pytest. В других средах (например, Pytest fixtures без monkeypatch) нужно использовать другие механизмы.

Тестирование асинхронного кода на pytest

Python поддерживает асинхронные функции (async def). Их тестирование требует особого подхода. pytest-asyncio — плагин для работы с async/await в тестах.

  • Установка:
pip install pytest-asyncio

Простой пример тестирования async-функции

import asyncio

@pytest.mark.asyncio
async def test_async_greeting():
    result = await greet_user("Alice")
    assert "Hello, Alice!" in result

async def greet_user(name):
    return f"Hello, {name}!"

Использование @pytest.mark.asyncio в тестах с фикстурами и mock'ами

import pytest
from unittest.mock import AsyncMock

def test_async_api_call():
    client = Client()
    response = await client.get_user("123")
    assert response.status == 200

@pytest.mark.asyncio
@pytest.fixture
def mocked_client(monkeypatch):
    async def mock_get(self, user_id):
        return MockResponse(status=200)
    monkeypatch.setattr(Client, "get_user", AsyncMock(side_effect=mock_get))
    yield Client()  # возвращается в тест

Тесты с asyncio.gather и параллельными операциями

@pytest.mark.asyncio
async def test_parallel_fetch():
    tasks = [
        fetch_user("a1"),
        fetch_user("b2"),
    ]
    results = await asyncio.gather(*tasks)
    assert len(results) == 2

Рекомендации по написанию качественных тестов

Чтобы тесты были надёжными и поддерживаемыми, следует придерживаться следующих принципов:

  • Маленькие тесты — один тест должен проверять одну вещь.

  • Читаемость имен — test_user_creation_valid лучше, чем просто test123.

  • Изоляция тестов — каждый тест не должен зависеть от состояния других (если это специально не задумано).

Пример плохой практики:

def test_order_total():  # зависит от предыдущих действий в модуле
    order = Order(items=[Item(...)])
    assert total(order) == expected  # может быть ложным, если предыдущий тест сломал данные

И пример хорошей практики (с фикстурой):

def test_order_total(order_fixture):
    assert calculate_total(order_fixture.items) == expected_value

Заключение: инструменты и лучшие практики

pytest предоставляет гибкие механизмы для тестирования Python-приложений:

  • Фикстуры — управление зависимостями.

  • Mocking через monkeypatch и unittest.mock — изоляция внешних зависимостей.

  • Параметризация тестов — экономия времени на написании тестовых случаев.

  • Поддержка асинхронного кода — через плагин pytest-asyncio.

Следуя этим подходам, можно создавать тесты, которые:

  • Быстро запускаются.

  • Легко поддерживаются.

  • Надёжно проверяют поведение приложения в различных условиях.

AI-Помощник