Тесты в проекте: разбор на живом примере API
Разберём условный сервис заказов: почему набор тестов на отдельные функции не ловит сбои в оплате и базе, какие проверки добавить и чем за них придётся платить. Без выдуманных замеров и универсального «покройте всё»

В сервисе заказов тест расчёта итоговой суммы проходит, а покупатель всё равно получает заказ без скидки: API прочитал промокод, но при сохранении заказа потерял его. На условном Python-проекте разберём, какие проверки ловят такой дефект, где они проходят границу между быстрыми и дорогими и как решать, что тестировать первым. Это пример для обсуждения, а не отчёт о реальном инциденте; чисел производительности у него нет.
Сценарий: заказ проходит через несколько слоёв
Представим API магазина. Клиент отправляет товары и промокод, сервер проверяет остатки, рассчитывает цену, сохраняет заказ в PostgreSQL и передаёт событие для дальнейшей обработки. Разработчик уже написал тесты на функции расчёта. Но они вызываются напрямую и не проверяют ни разбор запроса, ни транзакцию, ни сохранённые данные.
В этом и состоит конфликт: чем ближе тест к настоящему пользовательскому сценарию, тем больше он способен проверить за раз, но тем дороже его подготовить и тем сложнее найти причину падения. Если же тестировать только маленькие функции, обратная связь будет быстрой — и важный дефект на стыке компонентов может пройти незамеченным.
Тест полезен не потому, что проверяет строку кода, а потому, что ловит рискованный для пользователя сценарий.
Для примера используем Python 3.12 и pytest 8.x. Это не единственно возможный стек: если проект уже использует другой раннер, менять его только ради этой схемы не нужно.
Три уровня проверки и их цена
В проекте удобно разделить проверки по границе, которую они проходят. Названия уровней в командах различаются, поэтому важнее не ярлык, а то, какие реальные зависимости задействованы.
| Уровень | Что проверить в сценарии заказа | Что выигрываете | Цена и риск |
|---|---|---|---|
| Функциональный | Расчёт скидки, округление, запрет отрицательной суммы | Быстрый запуск; ошибка обычно указывает на конкретное правило | Не проверяет HTTP, базу и соединение компонентов |
| Интеграционный | Сохранение заказа и скидки в тестовой PostgreSQL; откат при ошибке | Проверяет контракт между кодом и базой | Нужна отдельная тестовая среда; запуск и настройка сложнее |
| Сквозной | Запрос проходит через API, создаёт заказ, возвращает ожидаемый ответ | Проверяет пользовательский путь целиком | Больше зависимостей; падение может потребовать диагностики нескольких слоёв |
Сквозной тест не заменяет тесты правил скидки. Если он упал, причина может быть в маршруте, базе или сериализации ответа. Отдельная проверка расчёта локализует ошибку быстрее. И наоборот: зелёный тест функции не доказывает, что приложение действительно сохранит скидку в заказе.
Как разложить один живой сценарий
Начните не с процента покрытия, а с пути, за который отвечает продукт. В нашем примере это заказ с допустимым промокодом. Его можно разрезать на проверки, каждая из которых отвечает на конкретный вопрос.
- Проверка правила цены. Передайте функции корзину и действующий промокод, проверьте итоговую сумму и применённую скидку. Добавьте границу: скидка не должна сделать итог отрицательным. Результат понятен, если тест проверяет наблюдаемое значение, а не внутренний вызов функции.
- Проверка границ API. Отправьте корректный запрос через тестовый клиент приложения и проверьте код ответа и форму JSON. Отдельно проверьте отсутствующий или неизвестный промокод — ожидаемое поведение задаёт контракт продукта. Проверка прошла, если клиент получает именно документированный ответ, а не случайное внутреннее исключение.
- Проверка базы. Создайте заказ в изолированной тестовой PostgreSQL и прочитайте сохранённую запись. Убедитесь, что цена и скидка совпадают с ответом API. Проверка прошла, если данные после сохранения соответствуют ожидаемому результату.
- Проверка отказа. Смоделируйте ошибку сохранения или сбой зависимого шага и проверьте, что система не оставляет заказ в неконсистентном состоянии. Уточните ожидаемый результат для конкретной архитектуры: например, откат транзакции не отменит уже отправленное во внешнюю систему сообщение. Проверка прошла, если состояние соответствует выбранному контракту отказа.
- Один сквозной путь. Выполните сценарий через публичную точку входа приложения — от запроса до ответа и сохранённой записи. Проверка прошла, если результат виден снаружи и не требует обращения к приватным деталям реализации.
Подготовка данных здесь важна не меньше самих утверждений. Тесты должны создавать собственные заказы и промокоды, а не зависеть от порядка запуска или общей базы разработчика. Иначе один тест изменит состояние, от которого незаметно зависит следующий.
Что ломается в такой схеме
Тест проходит только после запуска всей базы
Функциональные проверки цены не должны требовать PostgreSQL, если правило можно проверить отдельно. Разделите быстрый набор и интеграционные проверки: так разработчик получает короткую обратную связь, а команда сохраняет проверку реального взаимодействия с базой.
Тест проверяет внутреннее устройство вместо результата
Если тест требует, чтобы конкретный приватный метод вызывался ровно один раз, рефакторинг может сломать его без изменения поведения приложения. Проверяйте сумму, ответ API и состояние заказа. Проверки вызовов уместны, когда сам факт вызова — часть контракта, например для защиты от повторной оплаты.
Сквозная проверка зависит от внешнего сервиса
Если каждый запуск обращается к настоящему платёжному провайдеру или почте, тест может упасть из-за сети, лимита или тестовой учётной записи. Для основного набора изолируйте внешнюю границу и отдельно проверяйте интеграцию средствами тестовой среды поставщика. Не подменяйте этим проверку собственной логики.
Локально зелёное, в CI красное
Чаще всего стоит проверить версии зависимостей, переменные окружения, часовой пояс и порядок миграций. Запускайте тесты на чистой тестовой базе, а не на базе с остатками от предыдущих прогонов. Если тест нестабилен, найдите зависимость от времени, случайных данных или общего состояния; повторный запуск до зелёного результата проблему не исправляет.
Как выбирать, что тестировать первым
Для проекта заказов разумный порядок — не тот, где больше строк покрытия, а тот, где ранняя проверка уменьшает наиболее дорогой риск.
- Сначала правила, от которых зависит деньги или доступ: расчёт суммы, права на действие, идемпотентность операции.
- Затем места, где компоненты обмениваются данными: сериализация запроса, схема базы, транзакция.
- После этого — несколько сквозных сценариев, которые представляют ключевые действия клиента.
- Редкие вспомогательные функции тестируйте по мере изменений и риска, а не ради красивого процента покрытия.
Покрытие показывает, какая часть кода исполнилась во время тестов, но не доказывает, что проверки обнаружат неверный результат. Высокое значение не заменяет осмысленные сценарии. Для сравнения до и после изменений важнее смотреть, какие дефекты тесты поймали и сколько времени занимает обратная связь.
Термин: идемпотентность — свойство операции, при котором повтор одного и того же запроса не создаёт повторный эффект. Для создания заказа это может означать защиту от дубликата, если клиент повторил запрос после сетевого тайм-аута.
Что делать дальше
Выберите один путь, который чаще всего влияет на клиента или деньги, и зафиксируйте ожидаемый результат на каждом уровне: правило, интеграция с базой, внешний ответ. Затем добавьте проверки отказа и запускайте их в CI на той же версии Python и зависимостей, что использует проект. После этого измерьте длительность набора и разберите нестабильные тесты отдельно: цель — не максимальное число проверок, а надёжный сигнал о сломанном поведении.
Инвалидация кеша и нагрузочные испытания — отдельные задачи; здесь речь только о проверке сценария заказа и границ между его компонентами.
Тест и проверочная: Быстрые ответы с объяснением, почему остальные варианты неверны
Спросить за 10 ₽Источники: Документация pytest, Документация Python: unittest — инструменты тестирования
Частые вопросы
Нужно ли покрывать тестами каждую строку?
С чего начать, если в проекте вообще нет тестов?
Нужно ли тестировать на настоящей PostgreSQL?
Что делать, если тесты проходят локально, но падают в CI?
Стоит ли делать сквозной тест для каждого случая?
Можно ли использовать эту схему с другой версией pytest?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.