Тестирование бэкенда: от основ до CI/CD

Курс о том, как проектировать и выполнять тестирование серверной части приложений: от модульных и интеграционных тестов до контрактного и нагрузочного тестирования. Разберём инструменты, тестовые данные, работу с БД и очередями, а также автоматизацию в CI/CD и качество тестов.

1. Введение в тестирование бэкенда и тестовая стратегия

Введение в тестирование бэкенда и тестовая стратегия

Бэкенд — это часть системы, которая обрабатывает запросы, выполняет бизнес-логику, работает с данными и взаимодействует с другими сервисами. Пользователь не видит бэкенд напрямую, но ощущает его качество через скорость, стабильность, корректность ответов и безопасность.

Тестирование бэкенда — это проверка того, что сервис:

  • возвращает корректные ответы при разных входных данных
  • устойчив к ошибкам и некорректным сценариям
  • корректно интегрируется с БД, очередями, внешними API и другими сервисами
  • соответствует требованиям по производительности и безопасности
  • остаётся стабильным при изменениях и релизах
  • Эта статья задаёт общий язык и рамки: какие виды тестов бывают, что именно мы проверяем в бэкенде и как собрать тестовую стратегию, которая выдержит рост продукта и команды.

    Что именно мы тестируем в бэкенде

    Типичный бэкенд-сервис состоит из нескольких слоёв:

  • API-слой: HTTP/gRPC обработчики, валидация, сериализация
  • бизнес-логика: правила, расчёты, статусы, ограничения
  • доступ к данным: транзакции, запросы, миграции, кеш
  • интеграции: внешние API, брокеры сообщений, хранилища
  • инфраструктурные аспекты: конфигурация, логирование, метрики, ретраи, таймауты
  • Именно на этих границах чаще всего возникают дефекты: расхождение контрактов, неверная обработка ошибок, гонки в данных, неправильные таймауты, неучтённые сценарии.

    !Упрощённая карта слоёв и зависимостей, где чаще всего нужны разные типы тестов

    Почему нельзя ограничиться только end-to-end тестами

    Проверять систему только через «пользовательские» сценарии (E2E) кажется логичным, но на практике это приводит к проблемам:

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

    !Визуальная эвристика о балансе уровней тестов

    Источник концепции и практических рекомендаций: The Practical Test Pyramid (Martin Fowler)

    Уровни тестирования бэкенда

    Ниже — базовая классификация, которую мы будем использовать дальше в курсе.

    | Уровень | Что проверяем | Что обычно изолируем | Примеры для бэкенда | |---|---|---|---| | Unit | одну функцию/класс/модуль | БД, сеть, время, внешние сервисы | расчёт скидки, маппинг DTO, правила статусов | | Integration | взаимодействие нескольких модулей и инфраструктуры | часть внешних зависимостей | репозиторий + реальная БД, обработчик + очередь в тестовом контейнере | | Component / Service | сервис как «чёрный ящик» по API, но без реальных внешних систем | внешние API через стабы/моки | тесты REST/gRPC эндпоинтов с тестовой БД | | Contract | совместимость интерфейсов между сервисами | бизнес-реализацию другой стороны | consumer-driven контракт для внешнего API, схема сообщений | | End-to-End | пользовательский поток через всю систему | ничего (или минимум) | создание заказа → оплата → доставка с реальными интеграциями |

    В разных командах термины могут отличаться. Важно, чтобы в вашей стратегии было явно прописано: какие тесты у вас есть и какие риски они закрывают.

    Полезный словарь терминов: ISTQB Glossary

    Что такое тестовая стратегия

    Тестовая стратегия — это договорённость в команде о том:

  • какие риски качества мы считаем критичными
  • какие типы тестов и на каких уровнях их закрывают
  • где границы ответственности (разработчики, QA, DevOps)
  • как тесты запускаются (локально, в CI, перед релизом)
  • как выглядят критерии готовности и качества
  • Стратегия нужна не ради документа, а чтобы решения были последовательными: почему мы добавляем контрактные тесты, но не плодим E2E; почему для критичного модуля делаем больше интеграционных проверок; почему для каждого бага заводим регрессионный тест.

    Из чего состоит практичная стратегия для бэкенда

    Ниже — «скелет» стратегии, который можно применять почти в любом проекте.

    Цели и критерии качества

    Определите, что для продукта важнее всего:

  • корректность бизнес-логики
  • стабильность и отсутствие деградаций при релизах
  • производительность (время ответа, пропускная способность)
  • безопасность (утечки данных, авторизация)
  • наблюдаемость (логи, метрики, трассировка)
  • Хорошая практика — формулировать проверяемо. Например: «все публичные API возвращают согласованный формат ошибок» или «критичные операции идемпотентны при повторе запроса».

    Область тестирования и границы

    Зафиксируйте:

  • какие компоненты входят в зону ответственности команды
  • какие внешние системы считаются «чужими» и как вы их эмулируете
  • что именно вы тестируете через публичный API, а что — на уровне модулей
  • Это помогает избегать ситуаций, когда тесты «случайно» начинают зависеть от нестабильной внешней песочницы.

    Риски и приоритеты

    Практичный подход — строить стратегию от рисков. Примеры типичных рисков в бэкенде:

  • финансовые операции и расчёты
  • права доступа и разграничение данных
  • конкурентные обновления и транзакции
  • интеграции с платежами/уведомлениями
  • миграции БД и обратная совместимость API
  • Под риски выбираются уровни тестов: критичная логика получает больше unit и интеграционных тестов; интеграции — контрактные и компонентные проверки.

    Набор тестов по уровням и их роль

    Одна из рабочих конфигураций для микросервисного API:

  • Unit: максимум бизнес-правил и преобразований данных
  • Integration: репозитории, транзакции, работа с очередью/кешем в тестовом окружении
  • Component: проверка публичных эндпоинтов сервиса целиком (с тестовой БД)
  • Contract: фиксация соглашений с другими сервисами и внешними API
  • E2E: ограниченный набор «сквозных» критичных сценариев
  • Важно: стратегия — это не «чем больше тестов, тем лучше», а баланс скорости, надёжности и стоимости поддержки.

    Тестовые данные и воспроизводимость

    Бэкенд-тесты часто ломаются не из-за кода, а из-за данных. Стратегия должна ответить:

  • как создаются данные для теста: фикстуры, фабрики, seed
  • как гарантируется независимость тестов друг от друга
  • что делаем с временем: фиксируем now, используем фейковые часы
  • как очищаем состояние: транзакции, пересоздание схемы, отдельные базы
  • Принцип: тест должен быть воспроизводимым и независимым от порядка запуска.

    Работа с внешними зависимостями

    Для бэкенда критично определить политику:

  • что мокается (например, платежи) и на каком уровне
  • где используются стабы (например, фиксированные ответы внешнего API)
  • где поднимается реальная зависимость (например, PostgreSQL в контейнере)
  • Типичная ошибка — мокать слишком низко и получать тесты, которые «проходят», но сервис не работает в реальности.

    Нефункциональное тестирование (минимальный обязательный набор)

    Даже в первой версии стратегии стоит зафиксировать, как вы проверяете:

  • производительность: хотя бы базовые нагрузочные прогоны на критичных эндпоинтах
  • устойчивость: таймауты, ретраи, работа при недоступности зависимостей
  • безопасность: базовые проверки авторизации, отсутствие утечек в логах
  • Дальше в курсе эти темы будут расширяться и встраиваться в CI/CD.

    Автоматизация и CI/CD: где и что запускается

    Стратегия должна явно отвечать, какие проверки где выполняются:

  • локально у разработчика: unit, быстрые компонентные проверки
  • в pull request: unit + интеграционные + линтер/статический анализ
  • в nightly/по расписанию: расширенные интеграционные, часть E2E, нагрузочные smoke
  • перед релизом: минимальный «release gate» (набор обязательных проверок)
  • !Пример того, как уровни тестов раскладываются по этапам CI/CD

    Мини-пример: как «перевести» требование в проверки

    Требование: «Эндпоинт создания заказа должен валидировать входные данные и не создавать дубль при повторной отправке запроса».

    Кандидатный набор тестов по стратегии:

  • Unit: проверка правил валидации (обязательные поля, диапазоны), логика идемпотентности на уровне сервиса
  • Integration: проверка уникальных ограничений/транзакции в БД, корректная обработка конфликтов
  • Component: запрос к POST /orders и повтор того же запроса, проверка одинакового результата и отсутствия дублей
  • E2E: один сквозной сценарий «создание заказа → дальнейшая обработка» как smoke
  • Так стратегия помогает не «угадывать», а системно выбирать тесты.

    Типичные ошибки начинающих

  • Ставить цель «покрытие тестами 100%» вместо покрытия рисков и критичных веток логики
  • Делать ставку только на E2E и страдать от флаков и долгих прогонов
  • Писать unit-тесты, которые повторяют реализацию (и ломаются при любом рефакторинге)
  • Мокать инфраструктуру так сильно, что тесты не отражают реальность
  • Не фиксировать контракты и получать «внезапно сломалось» при изменении соседнего сервиса
  • Что будет дальше в курсе

    В следующих материалах курса мы будем последовательно углубляться:

  • как тестировать HTTP API: статусы, ошибки, схемы, идемпотентность
  • как тестировать интеграцию с БД и миграции
  • контрактное тестирование и совместимость между сервисами
  • тестирование очередей и асинхронных процессов
  • наблюдаемость, тестирование отказоустойчивости и базовая безопасность
  • сборка тестов в CI/CD, ускорение прогонов и качество релизных гейтов
  • Эта статья — опорная. Возвращайтесь к ней, когда будете выбирать: какой уровень теста нужен и почему он должен жить именно в пайплайне, а не только локально.

    2. Модульные тесты: изоляция, моки и TDD

    Модульные тесты: изоляция, моки и TDD

    Модульные (unit) тесты — самый быстрый и дешёвый способ проверять корректность бэкенда на уровне отдельных функций, методов или небольших модулей. В предыдущей статье про тестовую стратегию мы говорили, что unit-тесты обычно составляют основу «нижнего уровня» пирамиды: они быстро запускаются, хорошо локализуют причину падения и помогают смело рефакторить код.

    В этой статье разберём три ключевые темы:

  • изоляция в unit-тестах и что именно нужно изолировать
  • тестовые двойники (моки, стабы, фейки) и как выбирать подходящий
  • TDD (разработка через тестирование) как практику проектирования кода и тестов
  • Что считается модульным тестом в бэкенде

    Unit-тест проверяет небольшой кусок логики в изоляции от внешнего мира.

    Обычно unit-тестами покрывают:

  • бизнес-правила (скидки, статусы, проверки ограничений)
  • преобразование данных (маппинг DTO, нормализация)
  • обработку ошибок и валидацию
  • детерминированные вычисления
  • Обычно не является unit-тестом:

  • тест, который ходит в реальную БД
  • тест, который обращается по сети к внешнему API
  • тест, зависящий от реального времени, случайности или окружения
  • Идея простая: unit-тест должен быть быстрым, повторяемым и точным в диагностике.

    Изоляция: что и зачем мы изолируем

    Изоляция означает, что результат теста должен зависеть только от вашего кода и входных данных, а не от внешних факторов.

    Чаще всего изолируют:

  • время (текущая дата, таймзоны)
  • случайность (генераторы случайных чисел)
  • сеть (HTTP/gRPC вызовы)
  • БД и кеш (PostgreSQL, Redis)
  • очереди (Kafka, RabbitMQ)
  • файловую систему
  • переменные окружения и конфигурацию
  • Если этого не сделать, возникают типичные проблемы:

  • тесты «флакают» (то проходят, то нет)
  • тесты становятся медленными
  • падения сложно расследовать
  • тесты нельзя запускать локально без большого окружения
  • !Как unit-тест изолирует бизнес-логику от внешних зависимостей

    Как писать код так, чтобы его было легко тестировать

    Большинство проблем с unit-тестами появляется не из-за инструментов, а из-за дизайна кода. Хорошие признаки «тестируемого» кода:

  • зависимости передаются извне (через параметры или конструктор), а не создаются внутри
  • бизнес-логика отделена от транспорта (HTTP) и инфраструктуры (БД)
  • функции по возможности детерминированы: одинаковый ввод → одинаковый вывод
  • Пример: плохой и хороший дизайн зависимости

    Плохой вариант: сервис сам создаёт клиента внешнего API и сам берёт текущее время.

    Проблема для unit-теста: вам придётся «подменять» и requests, и время, и ещё контролировать сеть.

    Хороший вариант: зависимости передаются извне.

    Теперь unit-тест может дать сервису фейковые зависимости и проверять только свою логику.

    Тестовые двойники: моки, стабы, фейки

    Чтобы изолировать код, используют тестовые двойники (test doubles) — заменители реальных зависимостей.

    Терминология в командах может отличаться, но полезно опираться на общую идею: мы заменяем зависимость на объект с контролируемым поведением.

    Полезные источники:

  • Test Double (Martin Fowler)
  • Mocks Aren’t Stubs (Martin Fowler)
  • Короткая таблица: что есть что

    | Двойник | Что делает | Когда использовать | Риск при злоупотреблении | |---|---|---|---| | Stub (стаб) | возвращает заранее заданные ответы | нужно стабилизировать входы (ответы внешнего сервиса, репозитория) | тесты могут не заметить, что вы неправильно используете зависимость | | Mock (мок) | позволяет проверять взаимодействие: что и как вызвали | важны эффекты: «должен вызвать отправку письма один раз» | тест привязывается к реализации и ломается при рефакторинге | | Fake (фейк) | упрощённая рабочая реализация (например, in-memory) | нужно реалистичное поведение без реальной инфраструктуры | фейк может расходиться с реальной системой |

    Важно: мок и стаб — это не «хорошо/плохо», это разные фокусы проверки.

    Practical rule: что проверять — состояние или взаимодействие

    Частый практический ориентир:

  • если вы можете проверить результат (возвращаемое значение, изменённое состояние) — делайте это
  • если результат увидеть трудно, но критично, что вызван внешний эффект (уведомление, публикация события) — проверяйте взаимодействие
  • Пример unit-теста со стабом и моком

    Ниже пример на pytest и unittest.mock. Мы тестируем бизнес-логику: если платёж успешно прошёл, нужно записать payment_id и отправить уведомление.

    Что здесь важно:

  • мы не делаем HTTP-запрос и не пишем в БД
  • тест проверяет контракт метода pay_order: что он возвращает и какие эффекты инициирует
  • зависимости заданы через конструктор, поэтому их легко заменить
  • Антипаттерны моков и как их избегать

    Тест повторяет реализацию

    Если тест проверяет слишком много внутренних вызовов, он начинает «ломаться от рефакторинга», даже когда внешнее поведение не менялось.

    Практика:

  • мокайте на границе вашего модуля (внешние сервисы, репозитории), а не внутренние функции доменной логики
  • чаще проверяйте результат и доменные инварианты, а не цепочку вызовов
  • Мок всего подряд

    Если вы замокали вообще всё, тест может стать бесполезным: он подтверждает, что мок ведёт себя так, как вы его настроили.

    Практика:

  • оставляйте чистую бизнес-логику без моков, где это возможно
  • для сложных зависимостей предпочитайте фейки (например, in-memory репозиторий), если они реалистичны
  • Общие моки и общие фикстуры на весь проект

    Глобальные моки часто приводят к «протечкам» состояния между тестами.

    Практика:

  • создавайте моки внутри теста или через фикстуры с гарантированным сбросом
  • избегайте скрытых зависимостей (когда часть моков настраивается «где-то в conftest», а тест этого не видно)
  • TDD: разработка через тестирование

    TDD (Test-Driven Development) — это подход, где вы пишете тесты до реализации и используете их как инструмент проектирования.

    Источник для общего понимания:

  • Test-driven development (Wikipedia)
  • Цикл TDD: Red → Green → Refactor

  • Red: пишем тест на желаемое поведение и убеждаемся, что он падает
  • Green: пишем минимальный код, чтобы тест прошёл
  • Refactor: улучшаем дизайн кода без изменения поведения (тесты остаются зелёными)
  • Ценность TDD для бэкенда:

  • помогает формализовать требования как проверяемое поведение
  • приводит к коду с явными зависимостями и хорошей тестируемостью
  • снижает страх рефакторинга, потому что тесты становятся «страховкой»
  • Мини-пример TDD для бизнес-правила

    Требование: «Скидка 10% применяется только если сумма заказа не меньше 5000».

    В TDD стиле:

  • пишете тесты на граничные значения (4999, 5000)
  • реализуете расчёт
  • рефакторите (например, выносите правило в отдельную функцию)
  • Пример теста:

    После этого появляется реализация:

    Да, это простой пример, но принцип тот же для более сложных правил: сначала фиксируем поведение, затем пишем код.

    Unit-тесты в общей стратегии курса

    Связь с тестовой стратегией из предыдущей статьи:

  • unit-тесты закрывают основной объём бизнес-логики и ошибок на ранней стадии
  • интеграционные и компонентные тесты позже подтвердят, что бизнес-логика корректно «встраивается» в БД, очереди и API
  • в CI unit-тесты обычно запускаются первыми и служат быстрым фильтром качества для pull request
  • Если вы строите стратегию «от рисков», то для критичных участков (деньги, права доступа, статусы) unit-тесты должны покрывать:

  • позитивные сценарии
  • негативные сценарии (ошибки, валидация)
  • граничные значения
  • Итоги

  • Unit-тесты проверяют маленькие куски логики и должны быть быстрыми, детерминированными и независимыми от окружения.
  • Изоляция достигается через явные зависимости и тестовые двойники.
  • Стабы удобны для управления входами, моки — для проверки эффектов, фейки — для упрощённых реалистичных реализаций.
  • TDD — это не только «про тесты», но и про дизайн: тестируемость появляется как результат правильных границ и зависимостей.
  • 3. Интеграционные тесты: БД, очереди, внешние сервисы

    Интеграционные тесты: БД, очереди, внешние сервисы

    Интеграционные тесты нужны там, где заканчивается чистая бизнес-логика и начинаются реальные границы бэкенда: база данных, очереди сообщений, внешние HTTP/gRPC сервисы, кеш, файловое хранилище. Если в предыдущей статье про модульные тесты мы изолировали код от инфраструктуры моками и фейками, то здесь мы делаем следующий шаг: проверяем, что ваш код действительно корректно работает вместе с инфраструктурой.

    Главная цель интеграционных тестов в бэкенде: снизить риск ситуации "unit-тесты зелёные, а в проде не работает".

    Что такое интеграционный тест в бэкенде

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

    Обычно интеграционный тест отвечает на вопросы:

  • правильно ли сервис читает и пишет данные в БД, с учётом схемы, ограничений и транзакций
  • корректно ли обрабатываются сообщения в очередях и повторные доставки
  • корректно ли сервис работает с внешними API: таймауты, ретраи, форматы ошибок, заголовки, авторизация
  • Важно отличать уровни тестирования из первой статьи:

  • Unit изолирует всё внешнее и проверяет логику в вакууме
  • Integration проверяет связку "наш код + инфраструктура" на границе (например, репозиторий + реальная PostgreSQL)
  • Component/Service чаще тестирует сервис как чёрный ящик по API (например, POST /orders + тестовая БД)
  • !Схема окружения для интеграционных тестов

    Почему интеграционные тесты сложнее unit-тестов

    Интеграционные тесты дороже и медленнее, потому что появляется реальное состояние и реальные протоколы.

    Типичные источники проблем:

  • состояние "протекает" между тестами (данные в БД, сообщения в очереди)
  • появляются тайминги (асинхронная обработка, ретраи)
  • тест зависит от окружения (версии БД, конфигурации, сетевые порты)
  • ошибки сложнее локализовать без логов, метрик и понятного разбиения тестов
  • Поэтому интеграционные тесты должны быть:

  • воспроизводимыми (одинаковый результат при любом порядке запуска)
  • управляемыми по данным (понятно, что создано и когда очищается)
  • ограниченными по объёму (они не должны заменять весь набор unit-тестов)
  • Интеграционные тесты с базой данных

    База данных часто ломает предположения unit-тестов: реальная схема, индексы, ограничения, транзакции, типы и таймзоны ведут себя не как "памятный список в коде".

    Что имеет смысл проверять с реальной БД

  • корректность SQL-запросов и маппинга результатов
  • транзакционность (например, "всё или ничего")
  • ограничения целостности: NOT NULL, UNIQUE, внешние ключи
  • конкурентные обновления (оптимистичные версии, блокировки)
  • миграции схемы и обратную совместимость
  • Базовые стратегии управления тестовой БД

  • отдельная база/схема на тестовый прогон
  • очистка данных между тестами
  • изоляция через транзакции с откатом
  • Практический выбор зависит от стека и требований к скорости.

    Очистка состояния: основные подходы

  • транзакция на тест и ROLLBACK в конце
  • пересоздание схемы (быстро, если миграции лёгкие)
  • TRUNCATE таблиц в известном порядке
  • Транзакция с откатом удобна, но не всегда применима:

  • если код сам управляет транзакциями
  • если тест проверяет поведение, завязанное на COMMIT
  • если есть фоновые воркеры, читающие данные параллельно
  • Реальная БД в контейнере

    Чтобы тесты были ближе к продакшену, часто поднимают БД в контейнере.

    Рабочие варианты:

  • поднимать зависимости через docker compose в CI
  • использовать Testcontainers, которые управляют контейнерами прямо из тестов
  • Полезные источники:

  • Docker Documentation
  • Testcontainers
  • Мини-пример: интеграционный тест репозитория (идея)

    Ниже пример структуры теста: мы не мокируем БД, а проверяем реальное сохранение и чтение.

    Ключевая мысль: такой тест должен падать, если сломалась схема, запрос, транзакция или маппинг.

    Интеграционные тесты с очередями и асинхронностью

    Очереди и брокеры сообщений (например, Kafka или RabbitMQ) добавляют сложности, которых почти нет в unit-тестах:

  • доставка может быть "как минимум один раз"
  • сообщения могут приходить не сразу
  • порядок сообщений не всегда гарантирован
  • обработчик должен быть идемпотентным
  • Какие свойства стоит проверять

  • что сервис публикует событие в нужный топик/очередь с нужным форматом
  • что воркер корректно обрабатывает сообщение и записывает результат
  • что повторная доставка не создаёт дублей
  • что ошибки приводят к ожидаемому поведению: ретрай, перенос в dead-letter очередь, логирование
  • Практика: тестируйте наблюдаемое поведение

    Вместо проверки "какие методы вызваны" (как часто бывает с моками), для очередей полезнее проверять наблюдаемые эффекты:

  • появилось сообщение в очереди
  • запись в БД изменилась
  • внешний эффект выполнен один раз
  • Как бороться с нестабильностью асинхронных тестов

  • используйте ожидания с таймаутом вместо sleep
  • храните корреляционные идентификаторы (например, event_id) и проверяйте по ним
  • делайте обработчики идемпотентными
  • отделяйте тесты публикации событий от тестов обработки
  • Принцип: тест должен быть медленным только там, где это неизбежно, и при этом оставаться предсказуемым.

    Интеграционные тесты внешних сервисов

    Внешние сервисы в тестах можно подключать тремя основными способами:

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

    Почему прямые вызовы во внешний сервис опасны

  • нестабильность сети и окружения
  • лимиты, квоты и ограничения песочницы
  • тестовые данные могут исчезать или меняться
  • сложность параллельного запуска тестов
  • Поэтому частая практика: в интеграционных тестах поднимать стаб внешнего API рядом, а реальные интеграции проверять отдельными прогоном или контрактными тестами.

    Что проверять на уровне интеграции со стабом

  • корректный URL, метод, заголовки, авторизация
  • корректная сериализация запроса
  • обработка ошибок: 4xx, 5xx, таймаут
  • ретраи и circuit breaker, если они есть
  • Инструменты для стабирования

    Конкретный выбор зависит от языка и стека. Универсальные варианты:

  • отдельный HTTP stub-сервис в docker compose
  • WireMock, если он подходит вашему стеку
  • Ссылки:

  • WireMock
  • Как выбрать, что мокать, а что поднимать реально

    Хорошее правило из тестовой стратегии: изоляция должна быть ровно такой, чтобы тест закрывал риск.

    Практическая таблица решений

    | Зависимость | Что чаще всего делают в интеграционных тестах | Что это закрывает | Типичный риск, если упростить | |---|---|---|---| | PostgreSQL/MySQL | поднимают реальную БД в контейнере | схема, типы, транзакции, SQL | "в памяти работало" | | Redis | иногда поднимают реальный Redis, иногда мокают | TTL, атомарность, конкурентность | неверные TTL/гонки | | Kafka/RabbitMQ | поднимают брокер в контейнере для ключевых сценариев | форматы событий, ретраи, повторная доставка | ошибки продакшен-пайплайна | | Внешний HTTP API | стаб-сервер + отдельные проверки контрактов | обработка ошибок, сериализация | внезапные изменения контракта |

    Организация интеграционных тестов в проекте

    Структура и именование

    Полезно явно разделять тесты по уровню:

  • tests/unit/ для unit
  • tests/integration/ для интеграций (БД, брокер)
  • tests/component/ для тестов API сервиса как чёрного ящика
  • Так проще настраивать запуск в CI и не смешивать разные скорости тестов.

    Тестовые данные

    Чтобы тесты были читаемыми и независимыми:

  • используйте фабрики или фикстуры для создания сущностей
  • избегайте зависимости от заранее "засиженной" базы
  • делайте данные минимальными: только то, что нужно для сценария
  • Логи и диагностика

    Интеграционные тесты должны давать материал для расследования:

  • логируйте запросы к БД и внешним сервисам в тестовом окружении
  • сохраняйте логи контейнеров при падении тестов в CI
  • фиксируйте корреляционные идентификаторы (request id, event id)
  • Как это ляжет в CI/CD

    Из первой статьи про стратегию следует типичное распределение:

  • локально: unit + часть быстрых интеграционных (если окружение поднимается быстро)
  • в pull request: unit + основные интеграционные с БД
  • nightly: расширенные интеграционные (очереди, больше сценариев, больше матрица версий)
  • Главная цель CI: дать быстрый и надёжный сигнал. Интеграционные тесты должны быть достаточно стабильными, чтобы не превращаться в источник постоянных "ложных" падений.

    Итоги

  • Интеграционные тесты проверяют взаимодействие вашего кода с инфраструктурой и выявляют классы дефектов, которые unit-тесты не видят.
  • Для БД важно проверять схему, транзакции, ограничения и миграции, а также управлять очисткой состояния.
  • Для очередей критичны асинхронность, повторные доставки и идемпотентность; тестировать лучше наблюдаемые эффекты.
  • Для внешних сервисов в интеграционных тестах чаще используют стабы, а реальные интеграции дополняют контрактными и отдельными прогонами.
  • В CI/CD интеграционные тесты запускают выборочно и осознанно, чтобы балансировать скорость и покрытие рисков.
  • 4. API-тестирование: REST/GraphQL, Postman и автотесты

    API-тестирование: REST/GraphQL, Postman и автотесты

    API-тестирование проверяет поведение бэкенда через его публичный интерфейс: HTTP REST, GraphQL или gRPC. В контексте курса это мост между уровнями тестирования из первых статей:

  • после unit-тестов (где мы проверяли логику в изоляции)
  • и рядом с интеграционными тестами (где мы проверяли границы с БД, очередями и внешними сервисами)
  • API-тесты часто относят к уровню component/service tests: мы запускаем сервис и проверяем его через API как чёрный ящик, при этом внешние зависимости могут быть тестовыми (например, тестовая БД, стаб внешнего API).

    Зачем тестировать API отдельно

    API — это контракт между клиентами и сервисом. Даже если бизнес-логика верна, проблемы часто возникают на уровне API:

  • неверные статусы HTTP и формат ошибок
  • поломанная сериализация и несовместимость полей
  • некорректная валидация входных данных
  • проблемы авторизации и утечки данных
  • несовместимость при изменениях версий
  • неучтённая идемпотентность и повторные запросы
  • API-тестирование даёт быстрый сигнал о регрессии в публичном поведении сервиса, что особенно важно перед CI/CD гейтами.

    !Жизненный цикл запроса и зоны риска для API-тестов

    Что именно проверять в API-тестах

    Практичный набор проверок для большинства эндпоинтов:

  • статус ответа и семантика ошибки
  • формат ответа и обязательные поля
  • валидация входных данных
  • авторизация и доступ к данным
  • идемпотентность и повторные запросы
  • пагинация, сортировка, фильтры
  • работа с временем и таймзонами в полях
  • совместимость контрактов при изменениях
  • Важно разделять что проверять и как:

  • поведение и контракт удобнее проверять API-тестами
  • детали алгоритма удобнее и дешевле проверять unit-тестами
  • корректность SQL, транзакций и ограничений удобнее фиксировать интеграционными тестами с реальной БД
  • REST API: базовые правила для тестов

    REST обычно строится вокруг ресурсов и стандартной семантики HTTP.

    Что считается корректным REST-ответом

    Минимальный чеклист для типового JSON API:

  • корректный статус HTTP
  • корректные заголовки, минимум Content-Type: application/json
  • стабильный формат тела ответа
  • предсказуемый формат ошибок
  • Статусы, которые важно тестировать

    | Сценарий | Типичный статус | Что проверить тестом | |---|---|---| | Успешное чтение ресурса | 200 | схема ответа, обязательные поля | | Успешное создание | 201 | тело ответа, Location при наличии | | Некорректный ввод | 400 | структура ошибки, сообщения по полям | | Нет прав | 401 или 403 | что данные не утекли в ответе | | Ресурс не найден | 404 | что нет подробностей, которые помогают атакующему | | Конфликт | 409 | полезно для уникальности и идемпотентности | | Внутренняя ошибка | 500 | формат ошибки, отсутствие секретов |

    Если команда фиксирует правила ошибок, их стоит оформить как контракт и проверять во всех API-тестах.

    Идемпотентность

    Идемпотентность означает: повтор запроса не создаёт нежелательных побочных эффектов.

    Типичные места, где это критично:

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

  • Отправьте запрос на создание.
  • Повторите тот же запрос.
  • Проверьте, что результат согласован, а дубликат не появился.
  • Часто применяют Idempotency-Key в заголовке, а на уровне БД — уникальные ограничения.

    Пагинация и сортировка

    Проблемы здесь часто проявляются только на реальных данных.

    Что проверять:

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

    GraphQL отличается от REST тем, что обычно запрос идёт на один HTTP endpoint, а структура ответа зависит от запроса.

    Официальная спецификация: GraphQL Specification

    Типовые проверки для GraphQL

  • корректность схемы и типов
  • проверка ошибок в errors и частичных ответов в data
  • проверка авторизации на уровне полей
  • устойчивость к слишком “дорогим” запросам
  • Ошибки и частичные ответы

    В GraphQL может быть ситуация, когда часть данных вернулась, а часть упала с ошибкой.

    Практика:

  • Тестом фиксируйте, какие ошибки ожидаемы.
  • Тестом фиксируйте, какие поля остаются null.
  • Проверяйте, что сервис не возвращает лишнюю диагностическую информацию.
  • Авторизация на уровне полей

    Частая ошибка: endpoint защищён, но отдельные поля в GraphQL утекли.

    Практика:

  • тест с ролью без прав должен получить отказ или null в конкретном поле
  • тест с ролью с правами должен получить значение
  • Ручное API-тестирование в Postman

    Postman полезен для быстрого исследования API, воспроизведения багов и подготовки коллекций, которые потом можно автоматизировать.

    Официальный сайт и документация: Postman

    Базовый рабочий процесс

  • Создайте запрос и проверьте базовую функциональность.
  • Вынесите параметры в переменные окружения.
  • Добавьте проверки в разделе Tests.
  • Соберите коллекцию и запустите прогон.
  • Окружения и переменные

    Обычно выделяют:

  • baseUrl
  • токены доступа
  • идентификаторы тестовых сущностей
  • Практика:

  • не хардкодьте URL и токены в запросах
  • храните разные окружения: local, stage
  • Пример простого теста в Postman

    Проверка, что ответ 200 и есть поле id:

    Прогоны коллекций и Newman

    Чтобы запускать Postman-коллекции в CI, используют Newman.

    Документация: Newman

    Пример запуска:

    Когда это удобно:

  • у команды уже много Postman-коллекций
  • нужно быстро добавить smoke-набор проверок в пайплайн
  • Ограничение: со временем большие Postman-наборы бывает сложнее поддерживать, чем тесты в коде рядом с сервисом.

    Автотесты API в коде

    Когда API-тесты становятся частью инженерного процесса, их часто переносят в код:

  • проще переиспользовать модели, фабрики, генерацию данных
  • проще делать общие проверки ошибок и схем
  • проще дебажить и поддерживать в review
  • Где эти тесты живут в стратегии

    Частая практичная конфигурация:

  • tests/unit/ покрывает правила и вычисления
  • tests/integration/ покрывает БД, брокер, внешние стабы
  • tests/component/ или tests/api/ покрывает сервис через HTTP или GraphQL
  • API-тесты отвечают на вопрос: если клиент вызовет наш сервис, что он увидит?

    Минимальный пример REST API-теста на Python

    Ниже пример с pytest и requests.

    Ключевые моменты:

  • есть таймаут
  • проверяем не только наличие данных, но и отсутствие чувствительных полей
  • Пример GraphQL API-теста

    Проверка схемы ответа и контрактов

    Для REST часто используют OpenAPI как источник истины.

    Спецификация: OpenAPI Specification

    Что можно тестировать:

  • эндпоинт существует и соответствует описанию
  • обязательные поля действительно есть
  • типы полей не “поплыли”
  • Для GraphQL источником истины обычно является схема GraphQL.

    Практика для команды:

  • хранить спецификацию рядом с кодом
  • обновлять её в рамках изменений API
  • добавлять проверки совместимости в CI
  • API-тесты и внешние зависимости

    API-тест как правило поднимает сервис целиком, поэтому важно решить, что делать с зависимостями:

  • БД обычно реальная, в контейнере, с изоляцией данных
  • внешние HTTP-сервисы обычно стабы
  • очереди поднимают реально только для критичных сценариев
  • Это продолжает идеи из статьи про интеграционные тесты: моки на низком уровне хороши для unit, но на уровне API часто нужно “ближе к реальности”.

    Что чаще всего ломает API-тесты и как это лечить

    Нестабильные тестовые данные

    Практики:

  • создавайте данные тестом явно
  • не полагайтесь на “предзагруженную базу”
  • очищайте состояние между тестами
  • Асинхронность и фоновые процессы

    Практики:

  • Не используйте sleep как основной механизм ожидания.
  • Используйте ожидания с таймаутом и проверкой условия.
  • Привязывайте проверки к корреляционному идентификатору.
  • Тесты привязаны к окружению

    Практики:

  • все URL, токены и настройки через переменные окружения
  • одинаковый способ поднять тестовое окружение локально и в CI
  • Минимальный “smoke” набор для CI

    Чтобы не перегрузить пайплайн, часто выделяют небольшой smoke-набор API-тестов.

    Пример состава:

  • health-check или readiness endpoint
  • один сценарий чтения защищённых данных
  • один сценарий создания сущности
  • один негативный тест на валидацию
  • Дальше, в темах про CI/CD, этот smoke становится частью релизного гейта, а расширенные прогоны уходят в nightly.

    Полезные источники и стандарты

  • OpenAPI Specification
  • GraphQL Specification
  • Postman
  • Newman
  • OWASP API Security Top 10
  • Итоги

  • API-тестирование проверяет контракт и наблюдаемое поведение сервиса через REST или GraphQL.
  • Для REST важно фиксировать статусы, ошибки, идемпотентность и пагинацию.
  • Для GraphQL важно тестировать схему, ошибки в errors, частичные ответы и авторизацию на уровне полей.
  • Postman удобен для ручной проверки и быстрых smoke-наборов, Newman помогает запускать коллекции в CI.
  • Для долгосрочной поддержки API-тесты часто переводят в код и встраивают в структуру unit + integration + api/component.
  • 5. Контрактное тестирование и совместимость сервисов

    Контрактное тестирование и совместимость сервисов

    В прошлых статьях курса мы закрывали риски на трёх уровнях:

  • unit-тесты защищают бизнес-логику в изоляции
  • интеграционные тесты подтверждают работу с инфраструктурой (БД, очереди, внешние сервисы)
  • API-тесты проверяют сервис через публичный интерфейс (REST/GraphQL)
  • Но в микросервисной архитектуре и даже в «модульном монолите» есть отдельный класс рисков: несовместимость интерфейсов между командами и сервисами. Именно для него существуют контрактные тесты.

    Контрактное тестирование отвечает на вопрос: если один сервис или клиент ожидает определённое поведение и формат данных, гарантирует ли другая сторона это поведение при изменениях и релизах?

    Что такое контракт и почему он ломается

    Контракт — это договорённость о взаимодействии между двумя сторонами:

  • потребитель (consumer): клиент, фронтенд, другой сервис, воркер
  • поставщик (provider): API-сервис, сервис событий, внешний провайдер
  • Контракт обычно включает:

  • URL/метод/заголовки (для HTTP)
  • формат запроса и ответа (поля, типы, обязательность)
  • статусы и формат ошибок
  • правила совместимости и версионирования
  • для событий: название топика/очереди, ключи, схема сообщения, семантика повторной доставки
  • Контракты ломаются чаще всего из-за:

  • удаления или переименования полей
  • изменения типа поля (например, id из числа в строку)
  • изменения семантики статуса или ошибки
  • добавления обязательного поля без дефолта
  • изменения поведения по умолчанию (например, сортировки или фильтра)
  • изменения событийной схемы или ключа партиционирования
  • Результат почти всегда одинаковый: одна сторона релизится, другая начинает падать или вести себя неверно.

    Контрактные тесты и их место в стратегии

    Контрактные тесты дополняют пирамиду тестирования:

  • unit и интеграционные тесты не знают, что именно ожидают внешние потребители
  • API-тесты часто проверяют «как сейчас работает сервис», но не фиксируют ожидания другой команды
  • E2E могут поймать проблему, но обычно поздно, дорого и с плохой диагностикой
  • Контрактные тесты дают:

  • ранний сигнал о несовместимости в pull request или в CI
  • возможность независимых релизов команд
  • уменьшение количества тяжёлых E2E ради проверки интерфейса
  • Полезные вводные материалы:

  • Consumer-Driven Contracts: A Service Evolution Pattern
  • Pact Documentation
  • Термины: consumer-driven и provider-driven

    Есть два распространённых подхода.

    Consumer-driven contracts (CDC)

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

    Практически это часто реализуется через Pact.

  • Pact
  • Provider-driven contracts

    Поставщик публикует спецификацию (например, OpenAPI), а потребители валидируют, что используют API корректно и что изменения не ломают совместимость.

  • OpenAPI Specification
  • На практике команды часто комбинируют оба подхода:

  • OpenAPI как «паспорт» публичного API
  • CDC для самых критичных интеграций и реально используемых сценариев
  • Что именно проверяют контрактные тесты

    Контрактные тесты проверяют интерфейс и наблюдаемое поведение, а не внутреннюю реализацию.

    Обычно контракт фиксирует:

  • минимальный набор полей, которые обязаны присутствовать
  • допустимые типы и форматы значений
  • статусы и структуру ошибок
  • правила авторизации на уровне «доступ разрешён/запрещён»
  • Контрактные тесты обычно не заменяют:

  • unit-тесты алгоритмов
  • интеграционные тесты транзакций и конкуренции в БД
  • нагрузочные тесты
  • Идея такая: контрактные тесты уменьшают риск «мы договорились иначе», а не риск «мы неправильно посчитали скидку».

    Как выглядит CDC-процесс в CI/CD

    Ниже типовой поток для CDC (на примере Pact):

  • Consumer пишет тест, который описывает ожидаемое взаимодействие.
  • Тест генерирует артефакт контракта (часто его называют pact file).
  • Контракт публикуется в хранилище (часто используют Pact Broker).
  • Provider в своём CI скачивает актуальные контракты и запускает верификацию.
  • Если верификация прошла, изменение считается совместимым.
  • !Схема жизненного цикла consumer-driven контрактов в CI/CD

    Ссылки по теме:

  • Pact Broker
  • Контракты для REST API

    Что фиксировать в контракте REST

    Практичный чеклист:

  • метод и путь (например, GET /users/{id})
  • обязательные заголовки (например, Authorization)
  • структура тела ответа и минимальные обязательные поля
  • статусы для типичных сценариев: успех, валидация, нет прав, не найдено
  • формат ошибок (единый объект ошибки)
  • Важно: контракт не должен «бетонировать» всё подряд. Если вы зафиксируете в контракте каждый внутренний технический заголовок и каждое поле, вы получите хрупкость вместо безопасности.

    Базовые правила совместимости для JSON

    Для типичных JSON-ответов полезно договориться о правилах:

  • добавление необязательного поля обычно совместимо
  • удаление поля обычно ломающе
  • изменение типа поля обычно ломающе
  • переименование поля обычно ломающе (лучше вводить новое и некоторое время поддерживать оба)
  • добавление нового обязательного поля в запрос обычно ломающе
  • Если команда использует OpenAPI, эти правила можно поддерживать через ревью спецификаций и автоматические проверки изменений.

    Контракты для событий и очередей

    Для асинхронных систем контракт ещё важнее, потому что:

  • потребитель может читать сообщения спустя часы/дни после публикации
  • одно событие могут использовать несколько потребителей
  • отладка «почему упал воркер» часто сложнее, чем отладка HTTP
  • Что фиксировать в контракте события

    Минимальный состав контракта сообщения:

  • имя топика/очереди и тип события
  • схема полезной нагрузки (payload)
  • идентификаторы: event_id, correlation_id (если приняты)
  • правила версионирования события
  • семантика доставки: возможны ли дубли, что считается идемпотентным ключом
  • Стандарты и спецификации:

  • AsyncAPI Specification
  • Правила совместимости для схемы сообщения

    Практичный подход:

  • добавляйте новые поля как необязательные
  • не меняйте смысл существующих полей «тихо» (лучше новое поле)
  • не удаляйте поля, пока есть активные потребители
  • для больших изменений вводите новую версию события или новый тип события
  • Контракты для GraphQL

    GraphQL часто воспринимают как «сам себя документирует», но совместимость там тоже ломается:

  • удаление поля или типа ломает клиентов
  • изменение nullability (из nullable в non-null) ломает клиентов
  • изменение аргументов резолвера ломает клиентов
  • Полезные источники:

  • GraphQL Specification
  • Практики для контрактов в GraphQL:

  • хранить схему в репозитории
  • автоматизировать проверку изменений схемы (schema diff) в CI
  • отдельно тестировать авторизацию на уровне полей (чтобы не было утечек)
  • Инструменты и техники контрактного тестирования

    Pact: когда подходит

    Pact полезен, когда:

  • есть несколько потребителей одного API
  • команды хотят релизиться независимо
  • нужно фиксировать реальные ожидания потребителей, а не «как задумано в документации»
  • Концептуально consumer тестирует себя с мок-поставщиком, генерируя контракт, а provider потом доказывает, что реально удовлетворяет этому контракту.

    Официальная документация:

  • Pact Documentation
  • OpenAPI как контракт и проверки совместимости

    Если у вас REST API и есть OpenAPI, это сильная база для provider-driven подхода:

  • спецификация становится «источником истины»
  • изменения можно ревьюить так же, как изменения кода
  • можно добавлять автоматические проверки на совместимость
  • Спецификация:

  • OpenAPI Specification
  • Практика: контракт в виде OpenAPI особенно полезен для:

  • публичных API
  • внутреннего API с большим количеством потребителей
  • ситуаций, где consumer не может легко писать CDC (например, сторонние клиенты)
  • Где контрактные тесты живут в пайплайне

    Типичное распределение по CI/CD (продолжает стратегию из первой статьи):

  • pull request:
  • - provider верифицирует контракты (или хотя бы критичный набор) - обновления OpenAPI/AsyncAPI проходят автоматическую проверку на breaking changes
  • nightly:
  • - расширенная матрица контрактов и окружений - проверки совместимости нескольких версий потребителей
  • перед релизом:
  • - контрактная верификация как обязательный gate для изменений API

    Типичные ошибки и как их избежать

    Контракт «слишком широкий»

    Если контракт фиксирует лишнее (неважные поля, случайные значения, внутренние детали), он начинает ломаться от легитимных изменений.

    Практика:

  • фиксируйте минимально необходимое поведение
  • используйте матчинг по типам и паттернам там, где значения не важны
  • Контракт «слишком узкий»

    Если контракт проверяет только статус 200, он не защищает от реальных регрессий.

    Практика:

  • добавляйте негативные сценарии: валидация, нет прав, не найдено
  • фиксируйте формат ошибок и важные поля
  • Контракты не встроены в процесс релиза

    Если контракты запускаются «иногда», они не выполняют роль страховки.

    Практика:

  • делайте контрактную верификацию обязательной частью CI для изменений в API
  • храните результат проверки как артефакт пайплайна
  • Практичный итоговый чеклист

    Перед тем как считать контрактную стратегию готовой, проверьте:

  • есть явная договорённость, что является контрактом (OpenAPI, Pact, AsyncAPI)
  • есть автоматическая проверка совместимости изменений
  • у ошибок API есть стабильный формат и он покрыт контрактами
  • для событий есть схема и правила версионирования
  • контрактная верификация встроена в CI/CD и влияет на релизный gate
  • Контрактные тесты не отменяют unit, интеграционные и API-тесты. Они закрывают отдельный риск: мы разошлись в ожиданиях и сломали друг друга при независимых релизах. Именно поэтому контрактное тестирование становится ключевым мостом от «тестируем сервис» к «безопасно релизим систему» — следующему шагу к CI/CD.

    6. Нагрузочное тестирование и профилирование производительности

    Нагрузочное тестирование и профилирование производительности

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

    В предыдущих статьях курса мы строили качество по слоям:

  • unit-тесты защищают бизнес-логику
  • интеграционные тесты защищают границы с БД/очередями/внешними сервисами
  • API-тесты фиксируют контракт и поведение через HTTP/GraphQL
  • контрактные тесты защищают совместимость сервисов
  • Нагрузочное тестирование и профилирование — следующий шаг: они отвечают на вопрос «как сервис ведёт себя при росте нагрузки и где именно он упирается в ресурс». Это напрямую связано с CI/CD, потому что деградации производительности часто появляются как регрессии от изменений кода, конфигурации или запросов к БД.

    Что мы считаем производительностью бэкенда

    В практике удобно говорить не абстрактно “быстро/медленно”, а через измеримые показатели.

    Основные метрики:

  • latency (задержка ответа): средняя и перцентили
  • throughput (пропускная способность): запросы в секунду
  • error rate (доля ошибок): HTTP 5xx/4xx, таймауты, ошибки бизнес-логики
  • saturation (насыщение ресурсов): CPU, память, пул соединений к БД, диск, сеть
  • Ключевая мысль: сервис может быть “быстрым” на одном пользователе, но “умирать” при насыщении ресурса (например, из-за пула соединений к БД).

    !Связь ключевых метрик производительности и типичная цепочка деградации

    Перцентили задержек и почему среднее вводит в заблуждение

    Для пользователей важно не только “в среднем”, а “как часто тормозит”. Поэтому в нагрузочных тестах почти всегда используют перцентили.

  • p50 — медианная задержка (половина запросов быстрее, половина медленнее)
  • p95 — 95% запросов быстрее этого значения
  • p99 — “хвост” задержек, который часто определяет реальное ощущение стабильности
  • Почему нельзя смотреть только среднее:

  • несколько “очень медленных” запросов могут почти не повлиять на среднее, но сильно ударить по p95/p99
  • хвосты часто появляются из-за конкуренции за ресурсы (пулы, блокировки, GC, дисковые операции)
  • Модель нагрузки: что именно мы симулируем

    Нагрузочный тест полезен только если он похож на реальную эксплуатацию. Для этого задают модель нагрузки.

    Что нужно определить до запуска:

  • какие эндпоинты и операции входят в профиль
  • доли операций (например, 70% чтение, 25% создание, 5% админские операции)
  • распределение по данным (например, популярные пользователи/товары встречаются чаще)
  • нужна ли “пауза между действиями” (think time)
  • какие зависимости участвуют: БД, кеш, очередь, внешний API
  • Практичный подход: начать с одного-двух критичных сценариев (денежных, “корзина/заказ”, авторизация), а потом расширять.

    Виды нагрузочного тестирования

    Ниже — базовые типы, которые чаще всего используют для бэкенда.

    | Тип | Что проверяем | Когда применять | Типичный результат | |---|---|---|---| | Smoke performance | что сервис вообще “дышит” под небольшой нагрузкой | после деплоя, в CI как быстрый сигнал | быстрый ранний фейл при явной проблеме | | Load test | поведение на ожидаемой нагрузке | перед релизом, регулярно по расписанию | соответствие целям по задержке/ошибкам | | Stress test | пределы и деградация при перегрузе | для планирования мощностей и отказоустойчивости | точка “коллапса”, характер деградации | | Spike test | реакция на резкий скачок | если бывают всплески трафика | проблемы с авто-масштабированием, очередями | | Soak test | устойчивость на длинной дистанции | чтобы поймать утечки памяти, рост очередей, деградации кеша | “ползучие” проблемы через часы |

    Важно: эти тесты отвечают на разные вопросы. Ошибка — пытаться одним прогоном закрыть всё.

    Как задавать цели: SLIs, SLO и простые пороги

    Для CI/CD удобны конкретные критерии “прошёл/не прошёл”. Обычно используют:

  • SLO по задержке: например, p95 < 200ms для GET /orders/{id}
  • SLO по ошибкам: например, “ошибки 5xx < 0.1%”
  • ресурсные пороги: например, “CPU < 80% при ожидаемой нагрузке”
  • Если в команде нет формализованных SLO, начните с:

  • базовой линии (baseline) на текущей версии
  • относительных порогов на регрессию (например, p95 не хуже более чем на 10%)
  • Базовые вычисления: throughput в запросах в секунду

    Иногда нужно быстро посчитать пропускную способность по результатам прогона.

    Если за время выполнено запросов, то средний throughput:

    Где:

  • — запросы в секунду (requests per second)
  • — количество завершённых запросов за измеряемый интервал
  • — длительность интервала в секундах
  • Это упрощённая метрика: она не показывает хвосты задержек и не заменяет перцентили.

    Инструменты для нагрузочного тестирования

    Выбор инструмента важен меньше, чем корректная модель нагрузки и воспроизводимость. Но некоторые инструменты особенно популярны.

    Часто используемые:

  • k6 — сценарии на JavaScript, хорошо подходит для CI
  • Apache JMeter — мощный, но “тяжелее” в поддержке
  • Gatling — сценарии на Scala, силён в отчётах и моделировании
  • Locust — сценарии на Python, удобен для сложной логики
  • Критерии выбора:

  • насколько удобно описывать ваши сценарии (авторизация, цепочки запросов)
  • насколько просто запускать в CI и хранить сценарии рядом с кодом
  • насколько хорошо снимаются метрики и строятся отчёты
  • Типовая структура сценария нагрузочного теста

    Хороший сценарий обычно включает:

  • разогрев (warm-up), чтобы прогрелись кеши и JIT (если он есть)
  • измеряемую фазу
  • плавное завершение (чтобы корректно собрать метрики)
  • И обязательно:

  • таймауты на запросы (иначе тест “зависает” и портит картину)
  • корреляцию данных (не хардкодить идентификаторы, которые быстро “ломаются”)
  • Пример идеи (псевдокод, без привязки к конкретному инструменту):

    Реалистичность окружения и данных

    Главная причина бесполезных нагрузочных тестов — не тот стенд.

    Практики, которые повышают доверие к результату:

  • близкие версии БД/брокера/кеша к продакшену
  • сопоставимые настройки пулов и лимитов
  • объём данных, похожий на реальный (индексы, селективность запросов)
  • контроль фоновых задач (воркеры, крон-задачи), чтобы понимать их вклад
  • Если тестовый стенд слабее продакшена, результаты можно использовать как регрессионный индикатор, но осторожно для “абсолютных” цифр.

    Профилирование: как находить узкие места

    Нагрузочный тест отвечает на вопрос «что стало плохо и при какой нагрузке». Профилирование отвечает на вопрос «почему стало плохо на уровне CPU/памяти/блокировок/запросов».

    Что обычно профилируют:

  • CPU: горячие функции, синхронизация, сериализация
  • память: утечки, рост аллокаций, давление на GC
  • I/O: сеть, диск, ожидание БД
  • БД: медленные запросы, отсутствие индексов, N+1
  • блокировки: мьютексы, транзакционные блокировки в БД, contention на пуле соединений
  • Профилирование и наблюдаемость

    Профилирование работает лучше, если включена базовая наблюдаемость.

    Минимальный набор:

  • метрики (latency, RPS, error rate, saturation)
  • трассировка (distributed tracing) для поиска медленных участков запроса
  • логи с корреляционными идентификаторами
  • Полезные подходы к метрикам:

  • RED (Requests, Errors, Duration) для сервисов
  • USE (Utilization, Saturation, Errors) для ресурсов
  • Описание RED и USE в документации Google SRE:

  • Site Reliability Engineering (книга)
  • Инструменты профилирования по стеку

    Примеры распространённых инструментов:

  • Go: pprof в стандартной библиотеке, описание в документации Go
  • - The Go Blog: Profiling Go Programs
  • Linux: perf для низкоуровневого профилирования
  • - perf (Linux man-pages)
  • Python: py-spy для sampling-профилирования
  • - py-spy (репозиторий)

    Отдельная категория — APM (например, OpenTelemetry + бекенд трассировки). В рамках курса важно понимать принцип: нагрузка + метрики + трассировки дают путь от симптома к причине.

    Практичный рабочий процесс: от симптома к фиксу

    Ниже — методика, которая хорошо сочетается с инженерной разработкой и CI.

  • Зафиксируйте baseline
  • Запустите нагрузочный тест и подтвердите проблему (например, рост p95)
  • Определите, какой ресурс насыщается (CPU, БД, пул, память)
  • Включите профилирование или трассировку и найдите “горячую” точку
  • Сделайте одно изменение (индекс, кэширование, оптимизация сериализации)
  • Повторите прогон и сравните результаты
  • !Цикл работы с производительностью: нагрузка → наблюдаемость → профилирование → улучшение

    Типичные причины деградаций в бэкенде

    Ниже — причины, которые чаще всего выявляют на практике.

  • Отсутствие индексов или неверные планы запросов
  • N+1 запросы из ORM
  • Слишком частая сериализация/десериализация больших объектов
  • Глобальные блокировки и contention (пулы, мьютексы, транзакции)
  • Неограниченные ретраи во внешние сервисы при деградации
  • Неправильные таймауты (слишком большие приводят к накоплению очереди)
  • Утечки памяти или рост кэшей без ограничений
  • Связь с предыдущими темами курса:

  • интеграционные тесты помогают ловить “реальные” запросы к БД и ошибки транзакций
  • API-тесты помогают фиксировать таймауты и корректные статусы/ошибки
  • контрактные тесты помогают избежать неожиданного разрастания payload (например, добавили обязательное поле и увеличили ответы)
  • Частые ошибки в нагрузочном тестировании

  • Тестируют один endpoint “в вакууме”, игнорируя фоновые задачи и конкуренцию
  • Сравнивают результаты разных прогонов без одинаковых входных данных и конфигурации
  • Запускают тест без warm-up и получают “ложно плохие” метрики
  • Используют sleep вместо ожидания условия в асинхронных сценариях
  • Не ставят таймауты и не отделяют сетевые проблемы от проблем сервиса
  • Как встроить нагрузочные проверки в CI/CD

    Полная нагрузка в каждый pull request обычно слишком дорогая. Но можно выстроить многоуровневую схему.

  • В pull request:
  • - performance smoke на небольшом профиле - сравнение с baseline или фиксированными порогами для критичных операций
  • Nightly (по расписанию):
  • - полноценный load/stress/soak (по необходимости) - прогон на более реалистичном стенде и с большим объёмом данных
  • Перед релизом:
  • - релизный gate по ключевым SLO (например, p95, error rate)

    Практика для устойчивости пайплайна:

  • храните сценарии нагрузки рядом с кодом
  • сохраняйте артефакты прогона (отчёты, сырые метрики)
  • сравнивайте тренды, а не только “один прогон”
  • Итоги

  • Нагрузочное тестирование проверяет поведение сервиса под нагрузкой: задержки, пропускную способность, ошибки и насыщение ресурсов.
  • Основные ориентиры — перцентили (p95/p99), error rate и saturation, а не только среднее время ответа.
  • Профилирование помогает найти причину деградации: CPU, память, I/O, БД, блокировки.
  • Лучший результат даёт связка: реалистичная модель нагрузки + наблюдаемость + воспроизводимый процесс “baseline → изменение → повторный прогон”.
  • В CI/CD обычно используют лёгкий performance smoke в PR и более тяжёлые прогоны по расписанию или перед релизом.
  • 7. Автоматизация в CI/CD: отчёты, качество и стабильность тестов

    Автоматизация в CI/CD: отчёты, качество и стабильность тестов

    Автоматизация тестирования в CI/CD превращает тесты из локальной привычки разработчика в системный механизм контроля качества релизов. В предыдущих статьях курса мы построили набор уровней проверок:

  • unit-тесты защищают бизнес-логику
  • интеграционные тесты подтверждают работу с БД/очередями/внешними зависимостями
  • API-тесты фиксируют наблюдаемое поведение сервиса
  • контрактные тесты защищают совместимость сервисов
  • нагрузочные тесты ловят деградации производительности
  • Теперь задача CI/CD — запускать эти проверки в нужное время, с правильными отчётами и с достаточной стабильностью, чтобы пайплайн был источником доверия, а не постоянного шума.

    Что значит качественный пайплайн тестов

    CI/CD для тестов обычно решает три цели одновременно:

  • Быстрая обратная связь: разработчик понимает, что сломалось, пока контекст свежий.
  • Надёжность сигнала: падение означает реальную проблему, а не случайность.
  • Управляемость: понятно, где смотреть отчёты, логи, артефакты и как воспроизвести локально.
  • Плохой пайплайн часто выглядит так:

  • тесты падают иногда и все привыкают перезапускать job
  • отчёт не показывает, что именно упало и почему
  • воспроизвести падение локально невозможно
  • Хороший пайплайн выглядит иначе:

  • уровни тестов разделены, быстрые проверки дают ранний фидбек
  • на каждое падение есть артефакты для диагностики
  • есть политика работы с флаками и деградациями
  • !Карта того, какие тесты обычно где запускаются в CI/CD

    Как разложить тесты по этапам CI/CD

    Практика из тестовой стратегии: чем тест быстрее и стабильнее, тем ближе он к pull request.

    Типовое распределение

    | Уровень проверки | Где запускать | Зачем именно там | |---|---|---| | Линтеры и статический анализ | pull request | ловят ошибки до выполнения тестов | | Unit-тесты | pull request | быстрый и точный сигнал о логике | | Интеграционные тесты (ключевой набор) | pull request | защита границ БД/очередей, ловит реальные проблемы схемы и запросов | | Контрактные тесты | pull request | защита совместимости между командами до релиза | | API smoke | pull request или после сборки сервиса | подтверждает, что сервис поднимается и отвечает по основным сценариям | | Расширенные API/E2E | nightly или перед релизом | дорого и медленно, но полезно как дополнительная страховка | | Нагрузочные smoke | nightly или отдельный performance pipeline | регрессии производительности нельзя ловить только функциональными тестами |

    Ключевой принцип: pull request должен давать быстрый и надёжный сигнал, иначе команда начнёт обходить систему.

    Отчёты: что обязательно сохранять и как их читать

    Тесты в CI ценны только тогда, когда результат можно быстро диагностировать.

    Минимальный набор артефактов на падение

  • Машиночитаемый отчёт тестов: чтобы CI мог показать статистику, список упавших тестов и историю.
  • Логи: приложения и зависимостей (БД, брокер, стаб-сервера), чтобы понимать контекст.
  • Дамп состояния по необходимости: например, конфигурация, версии контейнеров, миграции.
  • Часто используемый формат отчётов для CI — JUnit XML, потому что его поддерживают многие системы.

  • JUnit
  • pytest
  • Если нужно более человекоориентированное представление (шаги, вложения, скриншоты, запросы/ответы), часто используют отдельный репортинг.

  • Allure Report
  • Что должно быть видно в отчёте без чтения логов

  • какие тесты упали
  • текст ошибки и stack trace
  • длительность теста (медленные тесты часто становятся источником нестабильности)
  • категория теста (unit/integration/api/contract), чтобы понимать уровень
  • Что полезно добавлять к тестам для диагностики

  • корреляционный идентификатор запроса (request_id) в логах
  • сохранение HTTP запросов и ответов в упавших API-тестах
  • сохранение логов контейнеров тестового окружения
  • Если в сервисе есть трассировка, полезно уметь прикладывать trace-id к падению теста.

  • OpenTelemetry
  • Quality gates: как принимать решения пропускать релиз или нет

    Quality gate — это набор условий, без выполнения которых изменение нельзя слить или релизнуть.

    Примеры практичных гейтов

  • unit и интеграционные тесты должны быть зелёными
  • контрактная верификация должна быть зелёной для изменений API
  • API smoke должны быть зелёными после сборки/деплоя
  • запрещено увеличивать число флаки-тестов
  • Плохой гейт:

  • требовать 100% покрытия тестами как универсальную цель
  • Хороший гейт:

  • требовать покрытия критичных рисков (деньги, права доступа, транзакции, контракты)
  • Если команда использует покрытие, оно должно быть инструментом, а не самоцелью.

  • Coverage.py
  • Стабильность тестов: что такое флак и почему он опасен

    Флаки-тест — это тест, который может проходить и падать без изменения кода.

    Флаки разрушают доверие к CI: люди перестают реагировать на падения, начинают жать re-run и пропускают реальные дефекты.

    Как измерять флаки

    Можно использовать простую метрику доли флаки-запусков:

    Где:

  • — доля флаки-запусков
  • — число запусков, где тест падал нестабильно (например, при повторном запуске сразу проходил)
  • — общее число запусков
  • Эта формула полезна как индикатор тренда: даже если абсолютное значение небольшое, рост означает, что пайплайн деградирует.

    Типовые причины флаков в бэкенде

  • зависимость от реального времени и таймзон
  • гонки данных и параллельные тесты без изоляции
  • асинхронность и ожидания через sleep вместо ожидания условия
  • общий стейт между тестами (общая БД без очистки, разделяемые очереди)
  • сетевые таймауты и нестабильные внешние окружения
  • Эти причины напрямую связаны с предыдущими статьями:

  • unit-тесты должны изолировать время и сеть
  • интеграционные тесты должны управлять состоянием БД и брокеров
  • API-тесты должны быть детерминированными по данным и ожиданиям
  • Политика борьбы с флаками в CI

    Важна не только техника, но и процесс.

    Что обычно работает

  • Карантин флаки-тестов: временно исключить из gating-этапа, но продолжать запускать и собирать статистику.
  • Обязательный владелец теста: у флаки должен быть ответственный и срок исправления.
  • Запрет на бесконтрольные ретраи: ретраи могут маскировать реальные проблемы.
  • Ретраи: когда допустимы

    Ретрай может быть приемлем как временная мера, если:

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

    Ускорение пайплайна без потери качества

    Ускорять CI нужно аккуратно: иначе вы получите быстрые, но бесполезные проверки.

    Практики ускорения

  • Разделение тестов по уровням: не запускать тяжёлые E2E там, где достаточно unit+integration.
  • Параллельный запуск: распараллеливать тесты, но только при корректной изоляции данных.
  • Кеширование зависимостей: пакеты, Docker-слои, build cache.
  • Тест-селекция: запускать часть тестов по изменённым модулям, но иметь периодический полный прогон.
  • Параллелизм и изоляция

    Параллельный запуск почти всегда требует одного из подходов:

  • отдельная база/схема на каждый job
  • транзакционная изоляция на тест
  • уникальные префиксы для тестовых данных
  • Если это не сделать, параллельность превращается в фабрику флаков.

    Воспроизводимость: как сделать так, чтобы падение в CI повторялось локально

    Если падение нельзя повторить локально, исправление становится дорогим.

    Минимальные правила воспроизводимости

  • одинаковый способ поднять окружение локально и в CI (например, через docker compose)
  • фиксированные версии зависимостей (БД, брокер, язык, библиотеки)
  • единые команды запуска (например, make test-unit, make test-integration)
  • Инструменты зависят от стека, но идеи одинаковые.

  • Docker Compose
  • Наблюдаемость для тестов: логи, метрики, трассировки

    Когда тесты становятся системными, важно видеть не только что упало, но и где деградирует.

    Что полезно собирать в CI как метрики

  • длительность тестовых job и их тренды
  • топ самых медленных тестов
  • количество флаки-случаев
  • процент падений по уровням (unit/integration/api)
  • Почему это связано с производительностью

    Тесты тоже могут деградировать: медленнее становятся запросы к БД, увеличивается время старта сервиса, растут ответы. Если этого не отслеживать, nightly будет “ползти”, а потом станет непригодным.

    Для нагрузочных тестов особенно важно сохранять отчёты как артефакты и сравнивать тренды.

  • k6
  • Практичный шаблон CI/CD для бэкенда

    Ниже пример того, как можно оформить пайплайн на уровне принципов (реализация будет отличаться в GitHub Actions, GitLab CI или Jenkins).

  • GitHub Actions
  • GitLab CI/CD
  • Jenkins
  • Pull request pipeline

  • линтеры и статический анализ
  • unit-тесты
  • интеграционные тесты с реальной БД в контейнере
  • контрактная верификация
  • API smoke (при необходимости)
  • Nightly pipeline

  • расширенные интеграционные (очереди, больше сценариев)
  • расширенные API и ограниченный E2E
  • performance smoke или профильный load test на стенде
  • Release pipeline

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

  • CI/CD делает тесты системой управления качеством: важны скорость обратной связи, надёжность сигнала и управляемость диагностики.
  • Отчёты и артефакты (JUnit XML, логи, метаданные окружения) критичны для расследования падений.
  • Quality gates должны защищать риски, а не превращаться в формальные KPI.
  • Флаки-тесты разрушают доверие к CI; нужна политика: измерение, карантин, владелец, исправление причин.
  • Ускорение пайплайна работает только вместе с изоляцией данных и воспроизводимым окружением.