Тестирование на pytest
Тестирование на pytest
pytest - автотесты в Python-проекте
Вы изменили функцию "нормализации заголовка" — и через неделю форма на сайте перестала сохранять данные. Вручную проверять десятки сценариев после каждого коммита утомительно.
Автотест — скрипт на Python, который:
- вызывает ваш код или HTTP API;
- сравнивает результат с ожиданием;
- печатает зелёный (всё ок) или красный (что-то сломалось).
Запуск одной командой pytest занимает секунды.
В Python есть встроенный unittest (классы TestCase). На практике чаще берут pytest — меньше шаблонного кода, удобные фикстуры, понятный вывод при падении.
| Где применять | Пример в энциклопедии |
|---|---|
| Чистые функции | normalize_title(), add_task() |
| HTTP API | FastAPI 3432, Flask 3411 |
| Django / DRF | pytest-django, APIClient |
Словарь
| Термин | Простыми словами |
|---|---|
| Тест | Функция test_*, которая что-то проверяет |
| assert | "Утверждение": если ложь — тест провален |
| Фикстура (fixture) | Подготовка перед тестом (БД, клиент API) |
| parametrize | Один тест — много наборов входных данных |
| TestClient | "Виртуальный браузер" к FastAPI без сети |
| coverage | Какой % кода хотя бы раз выполнился в тестах |
Что получится
Папка tests/ с идеями:
- простой
assertдля функции; - таблица примеров через
parametrize; - запросы к API через TestClient.
Установка и запуск
pip install pytest
Разбор:
- Команда устанавливает
pytestв активное окружение проекта. - После установки становится доступна консольная команда
pytest. - В учебных и рабочих проектах это минимальная база для автопроверок перед коммитом.
Структура проекта:
myapp/
main.py
logic.py
tests/
test_logic.py
test_api.py
Pytest сам находит файлы test_*.py и функции test_* внутри них.
pytest # все тесты
pytest -q # краткий вывод
pytest -v # имя каждого теста
pytest tests/test_logic.py::test_add_positive # один тест
Разбор:
pytestзапускает все найденные тесты.-qсокращает вывод,-vпоказывает подробные имена кейсов.- Формат
path::test_nameпозволяет точечно запускать один тест. - Точечный запуск ускоряет отладку, когда нужно проверить конкретный сценарий.
Запускайте из корня проекта, где видны и logic.py, и tests/. Иначе from logic import ... может дать ModuleNotFoundError.
Первый тест — чистая функция
Код приложения logic.py:
def normalize_title(title: str) -> str:
return title.strip()
Тест tests/test_logic.py:
from logic import normalize_title
def test_normalize_strips_spaces():
assert normalize_title(" hello ") == "hello"
def test_normalize_empty_becomes_empty():
assert normalize_title(" ") == ""
Разбор assert:
assert normalize_title(" hello ") == "hello"
Читается: "я утверждаю, что выражение слева равно выражению справа". Если нет — pytest покажет оба значения:
AssertionError: assert ' hello' == 'hello'
В unittest писали бы self.assertEqual(a, b) внутри класса. В pytest достаточно обычного assert.
parametrize — одна функция, много кейсов
Вместо копирования трёх функций — таблица входов и ожиданий:
Код ITЗагрузка примера кода…
| Элемент | Смысл |
|---|---|
@pytest.mark.parametrize | Декоратор pytest |
"raw, expected" | Имена аргументов теста |
| список кортежей | Три прогона = три строки в отчёте |
raw, expected | Подставляются в функцию автоматически |
Добавить граничный случай = одна строка в списке, а не новый файл.
Фикстуры — общая подготовка
Если десяти тестам нужен один и тот же "список заметок", копировать создание списка неудобно. Выносите в fixture:
import pytest
@pytest.fixture
def sample_notes():
return [{"id": 1, "text": "learn pytest"}]
def test_list_has_one_item(sample_notes):
assert len(sample_notes) == 1
assert sample_notes[0]["text"] == "learn pytest"
Разбор:
@pytest.fixtureобъявляет функцию-подготовку тестовых данных.sample_notesавтоматически подставляется в аргументы теста по имени.- Проверки
assert ...фиксируют ожидаемую структуру и содержимое данных. - Такой подход убирает дублирование setup-кода между тестами.
- Фикстуры помогают держать тесты короче и понятнее при росте проекта.
Как это работает:
- Pytest видит аргумент
sample_notesв тесте. - Ищет фикстуру с таким именем.
- Вызывает
sample_notes()перед тестом. - Передаёт результат в тест.
Фикстура с yield — и teardown
@pytest.fixture
def temp_file(tmp_path):
p = tmp_path / "data.txt"
p.write_text("hello", encoding="utf-8")
yield p
# после теста файл в tmp_path удалится автоматически
Разбор:
tmp_pathсоздаёт временную папку, уникальную для теста.- Код до
yieldвыполняет setup (подготовку файла). - Код после
yield— teardown, где выполняют очистку ресурсов. - Это снижает риск "грязного состояния" между тестами и делает прогон стабильнее.
Код до yield — setup, после — cleanup (закрыть соединение, откатить транзакцию).
conftest.py
Файл tests/conftest.py хранит фикстуры, общие для всей папки tests/ (клиент API, подключение к тестовой БД). Импортировать в каждом файле их не нужно — pytest подхватывает сам.
FastAPI — TestClient
Тестируем API без запуска uvicorn и без реального TCP — быстрее и стабильнее.
Код ITЗагрузка примера кода…
Разбор:
TestClient(app)запускает FastAPI-приложение в памяти без отдельного сервера.client.get(...)иclient.post(...)имитируют реальные HTTP-вызовы.json={...}сериализует тело запроса и ставит нужныйContent-Type.- Проверка
422подтверждает, что валидация входных данных работает корректно. - Такой стиль тестов быстрый и хорошо подходит для CI-пайплайнов.
| Выражение | Смысл |
|---|---|
client.get("/health") | Симулировать GET |
r.status_code | HTTP-код: 200, 201, 404, 422 |
r.json() | Распарсенное тело JSON |
json={"text": "..."} | Тело POST + заголовок Content-Type |
TestClient поднимает ASGI-приложение в памяти того же процесса, что и тесты.
JWT в тестах
Код ITЗагрузка примера кода…
Сначала получаем токен, потом передаём заголовок — как настоящий клиент после логина.
Flask — тестовый клиент
Код ITЗагрузка примера кода…
TESTING = True отключает некоторые "боевые" эффекты (например, строгие cookies). Паттерн тот же: фикстура client, запросы get/post.
Что тестировать в первую очередь
- Бизнес-логика без HTTP — быстрые тесты, мало зависимостей.
- HTTP-контракт — коды 200, 400, 401, 422 и форма JSON.
- Coverage (
pytest --cov=myapp) — ориентир, а не цель 100 % на старте. Сначала критичные пути — оплата, регистрация, сохранение данных.
Частые ошибки
| Симптом | Причина | Решение |
|---|---|---|
ModuleNotFoundError | Запуск не из корня | cd в корень или pip install -e . |
| Тесты видят "старые" данные | Общий список в модуле мутируется | Новая фикстура на каждый тест |
| 401 в API-тестах | Нет заголовка Authorization | Фикстура auth_headers |
| Падает только в CI | Читает production .env | monkeypatch.setenv в тесте |
monkeypatch (встроенная фикстура pytest) подменяет переменные окружения на время теста.
Что попробовать
pip install pytest-cov
pytest --cov=myapp
Разбор:
pytest-covподключает сбор метрики покрытия кода.pytest --cov=myappпоказывает, какая доля модулейmyappреально исполняется тестами.- Метрика помогает находить критичные участки без автотестов.
- Покрытие стоит использовать как ориентир качества, а не как единственную цель.
- Встроенная фикстура
tmp_path— файлы во временной папке. pytest.raises(ValueError)— ожидаемое исключение:
import pytest
def test_divide_by_zero():
with pytest.raises(ZeroDivisionError):
1 / 0
Связанные материалы
- FastAPI — первая программа
- Flask — первая программа
- FastAPI и база данных
- Чек-лист Python (блок тестирования)
- Рекомендации по разработке
Как строить тесты в реальном проекте
Когда проект растёт, тесты удобно раскладывать по уровням. Это снижает хаос и делает отчёт pytest читаемым даже через полгода.
| Уровень | Что проверяет | Инструменты |
|---|---|---|
| Unit | Отдельную функцию без сети и БД | pytest, parametrize, monkeypatch |
| Integration | Связку модулей и БД | pytest, тестовая БД, фикстуры |
| API | HTTP-контракт сервиса | TestClient, статус-коды, JSON-схемы |
| Smoke | Критические сценарии после деплоя | небольшой набор быстрых тестов |
Практический подход:
- Для каждой новой функции добавляйте хотя бы один unit-тест.
- Для каждого нового эндпоинта добавляйте успешный и неуспешный API-кейс.
- Для багов из продакшена сначала пишите тест, который воспроизводит ошибку, и только потом фикс.
Мини-чек-лист качества тестов
Перед коммитом прогоните короткую проверку:
- тесты запускаются командой
pytest -qбез ручной подготовки; - названия тестов читаются как сценарии (
test_create_note_without_text_returns_422); - в фикстурах нет скрытого глобального состояния;
- у ошибок проверяется код и структура
detail; - критические пути покрыты минимум одним позитивным и одним негативным сценарием.
Практикум по тестовым библиотекам
Responses — мок внешнего HTTP для requests
import requests
import responses
@responses.activate
def test_external_profile():
responses.get(
"https://api.example.com/profile/1",
json={"id": 1, "name": "Alice"},
status=200,
)
r = requests.get("https://api.example.com/profile/1", timeout=2)
assert r.json()["name"] == "Alice"
Разбор:
@responses.activateперехватывает HTTP-вызовыrequestsвнутри теста.responses.get(...)задаёт мок-ответ для конкретного URL.- Реальный
requests.get(...)не уходит в сеть, а получает подготовленный фиктивный ответ. assert ...проверяет бизнес-результат, а не транспорт.- Изоляция от сети делает тесты предсказуемыми и не флейковыми.
Freezegun — фиксированное время
from datetime import datetime
from freezegun import freeze_time
@freeze_time("2026-01-15 10:00:00")
def test_invoice_date():
assert datetime.now().strftime("%Y-%m-%d") == "2026-01-15"
Разбор:
@freeze_time(...)замораживает "текущее время" в рамках теста.datetime.now()возвращает предсказуемое значение, независимое от реальных часов.- Тест на даты перестаёт быть flaky и стабильно проходит в CI.
- Это особенно полезно для логики дедлайнов, TTL и окон активности.
Faker и factory_boy для фабрик
from faker import Faker
import factory
fake = Faker("ru_RU")
class UserFactory(factory.DictFactory):
name = factory.LazyFunction(fake.name)
email = factory.LazyFunction(fake.email)
Разбор:
Fakerсоздаёт реалистичные тестовые значения.factory.DictFactoryоформляет единый шаблон тестовой сущности.LazyFunctionгенерирует новые значения при каждом вызове фабрики.- Такой подход уменьшает ручное копирование фикстур и ускоряет написание тестов.
- Фабрики помогают централизованно менять структуру тестовых данных в одном месте.
pytest-asyncio для async-кода
import pytest
@pytest.mark.asyncio
async def test_async_sum():
async def add(a: int, b: int) -> int:
return a + b
assert await add(2, 3) == 5
Разбор:
@pytest.mark.asyncioзапускает тест в asyncio event loop.async def ...позволяет проверять асинхронный код в натуральном формате.await add(...)дожидается результата корутины.- Такой шаблон используют для тестирования async-сервисов, репозиториев и клиентов API.
- Без этого маркера корутины не выполнятся корректно в обычном синхронном тест-раннере.
Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.