Всем привет, меня зовут Сергей Прощаев и в этой статье расскажу про тестирование микросервисов.
Руковожу направлением Java-разработки в FinTech, в Отус на курсе «Микросервисная архитектура» веду направление тестирования в распределенных системах. Когда у тебя в хозяйстве 50+ сервисов, каждый со своей базой, очередями и кэшами, старый добрый подход «поднять всё локально и прогнать регресс» выглядит не реализуемым. Иногда команды тратят по две-три недели на прогон интеграционных тестов перед релизом. Есть истории, в которых падает прод, из-за того, что один сервис стал отвечать на 200 мс медленнее, и это убило таймауты в трёх соседних.
Сегодня разберем, как этого избежать. Поговорим про пирамиду тестирования для распределенных систем, про то, чем нагрузочное тестирование микросервисов отличается от нагрузочного тестирования монолита, и про best practices, которые реально работают в крупных проектах. А в конце традиционно расскажу, где можно пощупать это всё руками, не сломав прод.
Проблема: монолитные привычки в микросервисном мире
Представьте команду, которая вчера резала монолит. У них был staging, один на всех, они поднимали его раз в месяц и гоняли сотни end-to-end тестов. Приходит эта команда в микросервисы и делает то же самое: поднимает 40 сервисов в docker-compose и пытается прогнать те же сценарии.
Что происходит?
Нестабильные тесты (flaky test). Тест падает не потому, что код сломан, а потому, что сервис рекомендаций не успел стартануть, а сервис нотификаций сходил в недоступный внешний API. Команда тратит часы на перезапуски.
Время. Полный прогон занимает сутки. Разработчик уходит домой (
если он не работает удаленно из дома), не узнав результат.Раздутость. Эти тесты проверяют всё: от цепочки регистрации до изменения пароля. Но если упал один маленький сервис, непонятно, где именно искать проблему.
Это классическая ошибка — перетаскивание "пирамиды тестирования" из монолита без её переосмысления.
Перестраиваем пирамиду: теперь она выглядит иначе
Майк Кон придумал пирамиду (UI -> Service -> Unit) давно. В мире микросервисов она трансформируется. Я для себя вывел такую структуру, которая покрывает большинство сценариев тестирования:
Уровень 1 (Основание): Модульные тесты (Unit Tests). Тут без изменений. Каждый микросервис должен быть покрыт юнит-тестами изнутри. Проверяем бизнес-логику, хэндлеры, мапперы. Изоляция полная (все зависимости мокаются). Это быстро и надежно.
Уровень 2 (Середина): Интеграционные тесты (Integration Tests) в изоляции. Вот тут ключевой момент. Мы тестируем один микросервис, но с реальными интеграциями, которые поднимаем рядом. Базу данных? Поднимаем Testcontainers с PostgreSQL. Kafka? Поднимаем Redpanda в контейнере. Зависимый микросервис Auth? Не поднимаем его! Мокаем его ответы (WireMock), либо поднимаем его тестовый двойник. Задача — проверить, что наш сервис правильно ходит в БД, правильно пишет в очередь и правильно реагирует на ответы соседа.
Уровень 3 (Верхушка): Контрактное тестирование (Contract Testing). Это наша страховка от "незаметных изменений". Команда А и команда Б договариваются о формате общения (контракте). Автоматические тесты проверяют, что ни одна из сторон его не нарушила. Это намного быстрее, чем поднимать оба сервиса вместе.
Уровень 4 (Шпиль): Сквозное тестирование (E2E). Их должно быть мизерное количество. Только на критические сценарии (смоук-тесты). Например: "Пользователь может зарегистрироваться и оформить заказ". Для этого мы поднимаем не 40 сервисов, а выделенный стенд, который обновляется крайне осторожно.
Описанный вариант пирамиды тестирования приведен на рис. 1.

Важно: Интеграционные тесты в этой схеме — самые ценные. Они не такие хрупкие, как E2E, и проверяют реальное взаимодействие с реальными технологиями, а не с моками.
Интеграционное тестирование: как подружить код и Testcontainers
Давайте далее на примере. Пусть у нас есть сервис user-service, который пишет событие о новом пользователе в Kafka. Как это протестировать, не поднимая весь кластер?
Я использую Testcontainers. Это биб��иотека, которая позволяет из JUnit тестов поднимать Docker-контейнеры. Пишем тест на Java с использованием Spring Boot:
package ru.otus.userservice; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; import org.testcontainers.containers.KafkaContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.utility.DockerImageName; @SpringBootTest @Testcontainres class UserServiceIntegrationTest { @Container static final KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:latest")); @DynamicPropertySource static void overrideProperties(DynamicPropertyRegistry registry) { registry.add("spring.kafka.bootstrap-servers", kafka::getBootstrapServers); } @Test void testUserCreatedEventSentToKafka() { // Здесь код, который создает пользователя и проверяет, // что событие улетело в топик! } }
Перед тестом поднимается реальная Kafka, Spring Boot конфигурируется на работу с ней, тест дергает API, а затем проверяет, что сообщение легло в топик. Все минимально и быстро.
Нагрузочное тестирование: почему микросервисы падают не от нагрузки, а от её формы?
В монолите нагрузочное тестирование было простым: дергаем эндпоинт, смотрим, когда упадет. В микросервисах это сложнее.
В сети нашел описание интересного случая, когда в 2020 году команда проводила нагрузочное тестирование сервиса корзины (cart-service). Сервис держал все 1000 RPS (запросов в секунду). Наступила пятница — сервис лег через 10 минут.
При разборе полетов оказалось, что тестировали его наспех и изолированно. А в реальности за ним стоял inventory-service(сервис остатков) и payment-service. Когда пошел реальный трафик, cart-service начал слать запросы в inventory-service, а тот, не выдержав, стал отвечать с таймаутом в 5 секунд вместо 100 мс. Потоки в cart-service заблокировались, ожидая ответа, и через цепную реакцию положили всё.
Вывод: Нагрузочное тестирование микросервисов должно быть сетевым и каскадным. Мы тестируем не один сервис, а всю цепочку.
Еще мне понравилась практика подхода, который называется «нагрузка + хаос»:
Определяем критический путь. Например, «Добавить товар в корзину и оформить заказ».
Генерируем синтетический трафик на входной сервис (API Gateway) с помощью Gatling или JMeter. Важно генерировать реалистичный профиль нагрузки (ramp-up, разные сценарии).
Параллельно вносим хаос. С помощью инструментов типа Chaos Monkey или Gremlin отключаем один из зависимых сервисов, вносим задержку в сеть (network latency), «убиваем» одну из инстанций базы данных.
Смотрим, не посыпется ли всё.
Для лучшего понимания приведу диаграмму последовательности (Sequence Diagram) того, как выглядит типичная цепочка при нагрузке и где искать узкие места. Для рисования проверенным сервисом от Mermaid. То, что получилось изображено на рис. 2

Лучшие практики нагрузочного тестирования, описанные в разных источниках:
Тестируем не на пределе, а на пике. Важно знать не только точку отказа, но и поведение на 80% от этой точки. На графике, изображенном на рис. 3 это участок красной линии (Stress test) между «Expected load» и «Breaking point». Есть ли деградация? Растет ли время ответа линейно?
Ставим таймауты и circuit breakers. После того инцидента мы внедрили во все сервисы паттерн Circuit Breaker (через Resilience4j). Если зависимый сервис тупит, мы не ждем его вечно, а быстро отдаем fallback.
Автоматизация. Нагрузочные тесты должны крутиться в CI на каждый значимый коммит в критичных сервисах, хотя бы в облегченном режиме (smoke-load test), чтобы отлавливать регрессии производительности сразу.

Неочевидная практика: "Тестирование в проде"
Это звучит еретически, но современные зрелые команды обязательно тестируют микросервисы... в продакшене. Конечно, не на пользователях. Есть техника «темного запуска» (Dark Launching) или «теневого трафика» (Traffic Shadowing). В документации «теневой трафик» часто называют «теневым развертыванием».
Например, можно использовать это так: перед выкаткой новой версии payment-service, сначала копировать около 1% реального трафика с продакшена на новую версию (параллельно с основной), но результаты этой новой версии пользователю не отдаем, а сравниваем их с результатами старой. Это схема Теневого развертывания.
Затем переводим payment-service на Канареечный деплой и в этой схеме 1% пользователей получают реальный ответ от новой версии (Version 2.0) payment-service. Их поведение и ответы системы тестируются в реальных условиях.
Пример такой схемы приведен на рис. 4. Смотрим: не упала ли новая версия? Не вернула ли она другую сумму?

Плюс обязательный мониторинг «Золотых сигналов», изображенных на рис. 5:
Задержка (Latency)
Трафик (Traffic)
Ошибки (Errors)
Насыщенность (Saturation)
Под насыщенностью мы оцениваем степень загруженности системы — насколько близко она к пределу своих возможностей (по CPU, Memory, IO, Network). Это метрики, которые показывают, сколько ресурсов системы уже использовано и есть ли запас для обработки нагрузки.

Если после выкатки релиза время ответа сервиса выросло с 50 мс до 500 мс, мы откатываемся, даже если формальных ошибок нет. Это и есть тестирование производительности в реальном времени.
История из первых уст: как Amazon учил нас контрактам
Amazon здесь выступает как символ лучших индустриальных практик: компания, которая первой массово внедрила строгую контрактную дисциплину между сервисами (вспомните их знаменитый API-мандат Джеффа Безоса). Многие команды, набив шишки, приходят к тому же выводу, что и Amazon много лет назад — контракты должны быть кодом, а не текстом в вики!
В сети можно найти историю, которая произошла в крупном российском ритейле (название я сознательно не указываю, чтобы не светить NDA).
Если утрировать — то упрощенно эту историю можно рассказать так: перевод на микросервисы реализовывало 10 команд, контракты между сервисами были описаны в Wiki: «Команда А, вы шлете JSON вот такой, команда Б, вы его принимаете».
Через месяц Команда А добавила поле middleName в JSON профиля пользователя, посчитав это обратно совместимым. Команда Б, которая читала этот профиль, имела строгую схему и падала с DeserializationException, потому что их код не ожидал нового поля. И все узнали об этом через 20 минут после деплоя, когда посыпались ошибки.
В эту схему идеально ложиться использование Consumer-Driven Contract Testing (CDC) с помощью Pact. Идея гениальна в своей простоте:
Команда-потребитель (скажем, фронт или другой сервис) пишет тест, в котором говорит: «Я ожидаю от сервиса user-service ответ вот такого формата на такой-то запрос».
Этот тест генерирует файл-контракт, который кладется в репозиторий.
Команда-провайдер (владелец user-service) запускает тесты, которые проверяют, что его реальный API соответствует всем загруженным контрактам потребителей.
Такая практика тестирования смещает выявление проблем влево (влево по времени, к моменту разработки), а не к моменту интеграции или деплоя. При внедрении Pact, и количество конфликтов на стыке сервисов падает практически до нуля.
Заключение: тестирование как образ жизни
Тестирование микросервисов — это не набор инструментов, а философия. Это признание того, что ваша система сложна и распределена, и что она будет отказывать. Хорошие тесты — это ваша страховка.
Если резюмировать best practices, на которые стоило бы обратить внимание, то это будет список из пяти пунктов:
Сдвигайте тесты влево. Юниты и интеграционные тесты с Testcontainers должны быть основой.
Договаривайтесь контрактами. Pact или Spring Cloud Contract спасут от неожиданных поломок API.
Тестируйте под нагрузкой в связке. Изолированное нагрузочное тестирование бесполезно, а часто и вредно.
Внедряйте Chaos Engineering. Убейте один из сервисов на стейджинге сами, иначе это сделает прод.
Следите за метриками в проде. Это ваш главный нагрузочный тест.
И надо помнить, что: цель тестирования — не подтвердить, что код работает, а найти все способы, которыми он может сломаться!
Мы сегодня рассмотрели лишь часть практик, которые входят в курс «Микросервисная архитектура» в Отус. Программа курса позволяет закрыть все пробелы работы с распределёнными системами: от инфраструктурных основ (Docker, Kubernetes) и инструментов наблюдаемости (Prometheus, Grafana) до коммуникационных стратегий (Kafka, gRPC) и паттернов распределённых транзакций.
Курс даёт целостную картину разработчикам любого стека (Java, .NET, Python, Golang и др.), архитекторам и аналитикам, которые хотят проектировать масштабируемые и отказоустойчивые системы.

Когда микросервисов становится много, тестирование упирается не только в инструменты, но и в управляемость: что проверять в первую очередь, где автоматизация действительно окупается, как не утонуть в нестабильных проверках и разрозненных метриках. Курс Руководитель группы тестирования (QA Lead) помогает собрать эту систему целиком: выстроить процесс тестирования, оценивать трудозатраты и эффект изменений, принимать решения на основе качества продукта, а не ощущения контроля.
Для знакомства с форматом обучения и экспертами приходите на бесплатные уроки:
23 марта, 20:00. «ИИ в автотестах: помощник или угроза?». Записаться
30 марта, 20:00. «Spring Boot Actuator: основы мониторинга и управления приложением». Записаться
1 апреля, 20:00. «Создание интерфейсов с помощью OpenAPI». Записаться
