Блог AST-SoftPro
Тестирование 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.
Следуя этим подходам, можно создавать тесты, которые:
-
Быстро запускаются.
-
Легко поддерживаются.
-
Надёжно проверяют поведение приложения в различных условиях.