Всем привет, меня зовут Сергей Прощаев и в этой статье расскажу про тестирование микросервисов.

Руковожу направлением Java-разработки в FinTech, в Отус на курсе «Микросервисная архитектура» веду направление тестирования в распределенных системах. Когда у тебя в хозяйстве 50+ сервисов, каждый со своей базой, очередями и кэшами, старый добрый подход «поднять всё локально и прогнать регресс» выглядит не реализуемым. Иногда команды тратят по две-три недели на прогон интеграционных тестов перед релизом. Есть истории, в которых падает прод, из-за того, что один сервис стал отвечать на 200 мс медленнее, и это убило таймауты в трёх соседних.

Сегодня разберем, как этого избежать. Поговорим про пирамиду тестирования для распределенных систем, про то, чем нагрузочное тестирование микросервисов отличается от нагрузочного тестирования монолита, и про best practices, которые реально работают в крупных проектах. А в конце традиционно расскажу, где можно пощупать это всё руками, не сломав прод.

Проблема: монолитные привычки в микросервисном мире

Представьте команду, которая вчера резала монолит. У них был staging, один на всех, они поднимали его раз в месяц и гоняли сотни end-to-end тестов. Приходит эта команда в микросервисы и делает то же самое: поднимает 40 сервисов в docker-compose и пытается прогнать те же сценарии.

Что происходит?

  • Нестабильные тесты (flaky test). Тест падает не потому, что код сломан, а потому, что сервис рекомендаций не успел стартануть, а сервис нотификаций сходил в недоступный внешний API. Команда тратит часы на перезапуски.

  • Время. Полный прогон занимает сутки. Разработчик уходит домой (если он не работает удаленно из дома), не узнав результат.

  • Раздутость. Эти тесты проверяют всё: от цепочки регистрации до изменения пароля. Но если упал один маленький сервис, непонятно, где именно искать проблему.

Это классическая ошибка — перетаскивание "пирамиды тестирования" из монолита без её переосмысления.

Перестраиваем пирамиду: теперь она выглядит иначе

Майк Кон придумал пирамиду (UI -> Service -> Unit) давно. В мире микросервисов она трансформируется. Я для себя вывел такую структуру, которая покрывает большинство сценариев тестирования:

  1. Уровень 1 (Основание): Модульные тесты (Unit Tests). Тут без изменений. Каждый микросервис должен быть покрыт юнит-тестами изнутри. Проверяем бизнес-логику, хэндлеры, мапперы. Изоляция полная (все зависимости мокаются). Это быстро и надежно.

  2. Уровень 2 (Середина): Интеграционные тесты (Integration Tests) в изоляции. Вот тут ключевой момент. Мы тестируем один микросервис, но с реальными интеграциями, которые поднимаем рядом. Базу данных? Поднимаем Testcontainers с PostgreSQL. Kafka? Поднимаем Redpanda в контейнере. Зависимый микросервис Auth? Не поднимаем его! Мокаем его ответы (WireMock), либо поднимаем его тестовый двойник. Задача — проверить, что наш сервис правильно ходит в БД, правильно пишет в очередь и правильно реагирует на ответы соседа.

  3. Уровень 3 (Верхушка): Контрактное тестирование (Contract Testing). Это наша страховка от "незаметных изменений". Команда А и команда Б договариваются о формате общения (контракте). Автоматические тесты проверяют, что ни одна из сторон его не нарушила. Это намного быстрее, чем поднимать оба сервиса вместе.

  4. Уровень 4 (Шпиль): Сквозное тестирование (E2E). Их должно быть мизерное количество. Только на критические сценарии (смоук-тесты). Например: "Пользователь может зарегистрироваться и оформить заказ". Для этого мы поднимаем не 40 сервисов, а выделенный стенд, который обновляется крайне осторожно.

Описанный вариант пирамиды тестирования приведен на рис. 1.

Рис. 1. Уровни тестирования в современной разработке: от модульных тестов к сквозному тестированию (E2E)
Рис. 1. Уровни тестирования в современной разработке: от модульных тестов к сквозному тестированию (E2E)

Важно: Интеграционные тесты в этой схеме — самые ценные. Они не такие хрупкие, как 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 заблокировались, ожидая ответа, и через цепную реакцию положили всё.

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

Еще мне понравилась практика подхода, который называется «нагрузка + хаос»:

  1. Определяем критический путь. Например, «Добавить товар в корзину и оформить заказ».

  2. Генерируем синтетический трафик на входной сервис (API Gateway) с помощью Gatling или JMeter. Важно генерировать реалистичный профиль нагрузки (ramp-up, разные сценарии).

  3. Параллельно вносим хаос. С помощью инструментов типа Chaos Monkey или Gremlin отключаем один из зависимых сервисов, вносим задержку в сеть (network latency), «убиваем» одну из инстанций базы данных.

  4. Смотрим, не посыпется ли всё.

Для лучшего понимания приведу диаграмму последовательности (Sequence Diagram) того, как выглядит типичная цепочка при нагрузке и где искать узкие места. Для рисования проверенным сервисом от Mermaid. То, что получилось изображено на рис. 2

Рис. 2. Каскадный отказ в микросервисной архитектуре при деградации производительности зависимого сервиса
Рис. 2. Каскадный отказ в микросервисной архитектуре при деградации производительности зависимого сервиса

Лучшие практики нагрузочного тестирования, описанные в разных источниках:

  • Тестируем не на пределе, а на пике. Важно знать не только точку отказа, но и поведение на 80% от этой точки. На графике, изображенном на рис. 3 это участок красной линии (Stress test) между «Expected load» и «Breaking point». Есть ли деградация? Растет ли время ответа линейно?

  • Ставим таймауты и circuit breakers. После того инцидента мы внедрили во все сервисы паттерн Circuit Breaker (через Resilience4j). Если зависимый сервис тупит, мы не ждем его вечно, а быстро отдаем fallback.

  • Автоматизация. Нагрузочные тесты должны крутиться в CI на каждый значимый коммит в критичных сервисах, хотя бы в облегченном режиме (smoke-load test), чтобы отлавливать регрессии производительности сразу.

Рис. 3. Типы нагрузочного тестирования
Рис. 3. Типы нагрузочного тестирования

Неочевидная практика: "Тестирование в проде"

Это звучит еретически, но современные зрелые команды обязательно тестируют микросервисы... в продакшене. Конечно, не на пользователях. Есть техника «темного запуска» (Dark Launching) или «теневого трафика» (Traffic Shadowing). В документации «теневой трафик» часто называют «теневым развертыванием».

Например, можно использовать это так: перед выкаткой новой версии payment-service, сначала копировать около 1% реального трафика с продакшена на новую версию (параллельно с основной), но результаты этой новой версии пользователю не отдаем, а сравниваем их с результатами старой. Это схема Теневого развертывания.

Затем переводим payment-service на Канареечный деплой и в этой схеме 1% пользователей получают реальный ответ от новой версии (Version 2.0) payment-service. Их поведение и ответы системы тестируются в реальных условиях.

Пример такой схемы приведен на рис. 4. Смотрим: не упала ли новая версия? Не вернула ли она другую сумму?

Рис. 4 Схема проведения Канареечного деплоя
Рис. 4 Схема проведения Канареечного деплоя

Плюс обязательный мониторинг «Золотых сигналов», изображенных на рис. 5:

  • Задержка (Latency)

  • Трафик (Traffic)

  • Ошибки (Errors)

  • Насыщенность (Saturation)

Под насыщенностью мы оцениваем степень загруженности системы — насколько близко она к пределу своих возможностей (по CPU, Memory, IO, Network). Это метрики, которые показывают, сколько ресурсов системы уже использовано и есть ли запас для обработки нагрузки.

Рис. 5 Четыре золотых сигнала мониторинга
Рис. 5 Четыре золотых сигнала мониторинга

Если после выкатки релиза время ответа сервиса выросло с 50 мс до 500 мс, мы откатываемся, даже если формальных ошибок нет. Это и есть тестирование производительности в реальном времени.

История из первых уст: как Amazon учил нас контрактам

Amazon здесь выступает как символ лучших индустриальных практик: компания, которая первой массово внедрила строгую контрактную дисциплину между сервисами (вспомните их знаменитый API-мандат Джеффа Безоса). Многие команды, набив шишки, приходят к тому же выводу, что и Amazon много лет назад — контракты должны быть кодом, а не текстом в вики!

В сети можно найти историю, которая произошла в крупном российском ритейле (название я сознательно не указываю, чтобы не светить NDA).

Если утрировать — то упрощенно эту историю можно рассказать так: перевод на микросервисы реализовывало 10 команд, контракты между сервисами были описаны в Wiki: «Команда А, вы шлете JSON вот такой, команда Б, вы его принимаете».

Через месяц Команда А добавила поле middleName в JSON профиля пользователя, посчитав это обратно совместимым. Команда Б, которая читала этот профиль, имела строгую схему и падала с DeserializationException, потому что их код не ожидал нового поля. И все узнали об этом через 20 минут после деплоя, когда посыпались ошибки.

В эту схему идеально ложиться использование Consumer-Driven Contract Testing (CDC) с помощью Pact. Идея гениальна в своей простоте:

  1. Команда-потребитель (скажем, фронт или другой сервис) пишет тест, в котором говорит: «Я ожидаю от сервиса user-service ответ вот такого формата на такой-то запрос».

  2. Этот тест генерирует файл-контракт, который кладется в репозиторий.

  3. Команда-провайдер (владелец user-service) запускает тесты, которые проверяют, что его реальный API соответствует всем загруженным контрактам потребителей.

Такая практика тестирования смещает выявление проблем влево (влево по времени, к моменту разработки), а не к моменту интеграции или деплоя. При внедрении Pact, и количество конфликтов на стыке сервисов падает практически до нуля.

Заключение: тестирование как образ жизни

Тестирование микросервисов — это не набор инструментов, а философия. Это признание того, что ваша система сложна и распределена, и что она будет отказывать. Хорошие тесты — это ваша страховка.

Если резюмировать best practices, на которые стоило бы обратить внимание, то это будет список из пяти пунктов:

  1. Сдвигайте тесты влево. Юниты и интеграционные тесты с Testcontainers должны быть основой.

  2. Договаривайтесь контрактами. Pact или Spring Cloud Contract спасут от неожиданных поломок API.

  3. Тестируйте под нагрузкой в связке. Изолированное нагрузочное тестирование бесполезно, а часто и вредно.

  4. Внедряйте Chaos Engineering. Убейте один из сервисов на стейджинге сами, иначе это сделает прод.

  5. Следите за метриками в проде. Это ваш главный нагрузочный тест.

И надо помнить, что: цель тестирования — не подтвердить, что код работает, а найти все способы, которыми он может сломаться!

Мы сегодня рассмотрели лишь часть практик, которые входят в курс «Микросервисная архитектура» в Отус. Программа курса позволяет закрыть все пробелы работы с распределёнными системами: от инфраструктурных основ (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». Записаться