Лекция №10. Архитектура микросервисов и стратегии их поддержки. Контрактное тестирование.
§10.1. От монолита к микросервисам: как изменилось тестирование
В курсе «Основы Алгоритмизации и Программирования» вы уже знакомились с понятиями монолитной и микросервисной архитектуры.
Давайте коротко освежим память, потому что это напрямую влияет на то, как мы тестируем системы.
Монолитная архитектура, это когда вся система — это одно приложение. Все компоненты (интерфейс, бизнес-логика, работа с базой данных) находятся внутри одного проекта, собираются в один исполняемый файл и развертываются как одно целое.
Ключевая особенность данной архитектуры
Все компоненты внутри одного приложения. Если один компонент падает — падает вся система.
Схема монолитной архитектуры представлена ниже:

Микросервисная архитектура, это когда система разбита на множество маленьких, независимых приложений (микросервисов). Каждый микросервис отвечает за свою узкую область (например, «Пользователи», «Заказы», «Платежи»). Микросервисы общаются друг с другом по сети (через HTTP-запросы или очереди сообщений).
Ключевая особенность данной архитектуры
Каждый микросервис — это отдельное приложение. Если один микросервис падает — остальные продолжают работать.
Схема микросервисной архитектуры представлена ниже:

Для наглядности, давайте сравним эти две архитектуры в виде таблицы:
| Характеристика | Монолит | Микросервисы |
|---|---|---|
| Структура | Одно приложение | Много маленьких приложений |
| База данных | Одна БД для всех компонентов | Каждый сервис — своя БД |
| Развертывание | Всё приложение целиком | Каждый сервис независимо |
| Технологии | Один язык/стек для всего | Разные языки для разных сервисов |
| Масштабирование | Масштабируется целиком | Масштабируется только нужные сервисы |
Переход от монолита к микросервисам кардинально меняет то, как мы тестируем систему.
В монолите всё тестируется вместе. То есть выглядит это следующим образом:
1. Вы запускаете приложение (одно).
2. Оно поднимает базу данных.
3. Вы запускаете тесты.
4. Тесты проверяют всё: и UI, и логику, и работу с БД.
5. Всё это происходит в одном процессе.
Преимущества для тестирования данного вида архитектуры следующие:
- Просто. Одно приложение — один набор тестов.
- Быстро. Нет сетевых задержек между компонентами.
- Предсказуемо. Все компоненты всегда вместе.
но у такого вида есть и недостатки:
- Если тесты упали — непонятно, какой компонент сломался.
- Невозможно протестировать компонент отдельно.
- Любое изменение требует полного регрессионного тестирования.
В микросервисах же всё тестируется по отдельности. Как это выглядит:
1. Каждый микросервис тестируется независимо (модульные тесты).
2. Проверяется, что микросервис правильно работает со своей БД (интеграционные тесты).
3. Проверяется, что микросервис правильно общается с другими микросервисами (НОВАЯ ЗАДАЧА!).
4. Проверяется, что вся система работает вместе (E2E-тесты).
Преимущества для тестирования данного вида архитектуры следующие:
- Изолированность. Можно тестировать каждый сервис отдельно.
- Скорость. Тесты одного сервиса не зависят от других.
- Гибкость. Можно менять один сервис, не перетестируя все остальные.
но у такого вида есть и недостатки:
- Сложность. Нужно тестировать не только каждый сервис, но и их взаимодействие.
- Сеть. Появляются сетевые задержки и ошибки, которых нет в монолите.
- Версии. Сервисы могут развиваться с разной скоростью.
А как проверить, что сервисы правильно понимают друг друга? В монолите эта проблема не стоит. Все компоненты внутри одного приложения общаются через вызовы методов. Компилятор сам проверяет, что типы данных совпадают. В монолите это происходит все следующим образом:
csharp
// Всё в одном приложении
var order = new Order {
Id = 1,
Total = 100
};
// Компилятор проверит, что метод существует
orderService.Create(order);
В микросервисах же ситуация состоит иначе:
csharp
// Сервис А (заказы)
var order = new Order { Id = 1, Total = 100 };
var response = await httpClient.PostAsync(
"https://payment-service/pay",
order
);
// Сервис Б (платежи)
// Как проверить, что Сервис Б поймет этот JSON?
Появляется один логичный вопрос. Что же может пойти не так?
| Проблема | Пример |
|---|---|
| Разные версии | Сервис А обновился и отправляет новые поля, Сервис Б их не понимает |
| Разный формат | Сервис А отправляет {"orderId": 1}, Сервис Б ожидает {"id": 1} |
| Разные типы | Сервис А отправляет {"total": "100.00"}, Сервис Б ожидает число |
| Отсутствие поля | Сервис А перестал отправлять поле, Сервис Б ждет его |
Давайте приведем простую аналогию. Представьте, что два отдела в компании должны обмениваться отчетами.
- Отдел А отправляет отчет в Excel.
- Отдел Б ожидает отчет в Word.
Отделы не могут договориться о формате. Каждый раз, когда приходит отчет, отдел Б не может его открыть. Работа останавливается.
В микросервисах то же самое — сервисы общаются через API, и если они не договорились о формате данных, система не работает.
Именно здесь на помощь приходит контрактное тестирование. Давайте кратко разберем, что это такое.
Контрактное тестирование
— это способ проверить, что два сервиса (или более) правильно понимают друг друга.
Оно проверяет, что:
- Сервис А отправляет данные в том формате, который ожидает Сервис Б.
- Сервис Б отвечает в том формате, который ожидает Сервис А.
Для простой аналогии можно сказать следующее.
Контракт
— это как договор между двумя компаниями.
В нем записано:
Информация
Отдел А обязуется отправлять отчеты в формате Excel. Отдел Б обязуется принимать отчеты в формате Excel
Если кто-то нарушает договор — работа останавливается. В мире микросервисов это звучит следующим образом:
Информация
Сервис Заказов обязуется отправлять POST-запросы на /api/pay с JSON-телом { orderId, total }.
Сервис Платежей обязуется отвечать статусом 200 или 400.
Если один из сервисов нарушает этот контракт — система дает сбой. Контрактное тестирование автоматически проверяет, что контракты не нарушаются.
§10.2. Почему интеграционные и E2E-тесты не решают проблему микросервисов
В предыдущих лекциях мы изучили несколько видов тестирования:
| Вид тестирования | Что проверяет |
|---|---|
| Модульные тесты | Отдельные методы и классы |
| Интеграционные тесты | Связь с БД, внешними сервисами |
| E2E-тесты | Полные сценарии пользователя |
Естественный вопрос, который каждый может задать, это:
Интеграционные тесты проверяют, как один модуль взаимодействует с внешними системами — обычно с базой данных. Но они не проверяют взаимодействие между сервисами.
Пример интеграционного теста:
csharp
[Fact]
public async Task CreateOrder_ShouldSaveToDatabase()
{
// Проверяем, что заказ сохраняется в БД
var order = new Order {
UserId = 1,
Total = 100
};
await orderService.CreateOrderAsync(order);
var savedOrder = dbContext.Orders
.FirstOrDefault(o => o.UserId == 1);
Assert.NotNull(savedOrder);
}
Что он проверяет:
- Правильно ли работает репозиторий.
- Сохраняются ли данные в БД.
- Соблюдаются ли ограничения (внешние ключи, уникальность).
Чего он НЕ проверяет:
- Правильно ли сервис А вызывает API сервиса Б.
- Понимает ли сервис Б данные, которые отправляет сервис А.
- Совпадают ли форматы данных между сервисами.
E2E-тесты проверяют полный сценарий пользователя от начала до конца. Они запускают все микросервисы, открывают браузер и имитируют действия пользователя. Но несмотря на то, что они проверяют все вместе, они невероятно медленные и дорогие. Пример E2E-теста:
csharp
[Fact]
public async Task UserCanBuyProduct()
{
// 1. Открыть сайт
await page.GotoAsync("https://myshop.com");
// 2. Найти товар
await page.FillAsync("#search", "iPhone");
await page.ClickAsync("#search-btn");
// 3. Добавить в корзину
await page.ClickAsync(".add-to-cart");
// 4. Оформить заказ
await page.ClickAsync("#checkout");
await page.FillAsync("#card-number", "1234");
await page.ClickAsync("#pay");
// 5. Проверить подтверждение
await Expect(page.Locator(".confirmation")).ToBeVisibleAsync();
}
Что он проверяет:
- Всю систему целиком.
- Взаимодействие между всеми сервисами.
- Пользовательский опыт.
Чего он НЕ делает:
- Не дает быстрой обратной связи (медленный).
- Не помогает найти, какой именно сервис сломался.
- Не подходит для частого запуска в CI/CD.
Давайте вспомним, почему E2E тесты медленные и дорогие:
| Что происходит в E2E-тесте | Сколько времени |
|---|---|
| Запуск браузера | 2 – 3 секунды |
| Загрузка страницы | 1 – 2 секунды |
| Поиск элемента | 0.5 секунды |
| Клик, ввод текста | 0.5 секунды |
| Ожидание ответа сервера | 1 – 2 секунды |
| Загрузка следующей страницы | 1 – 2 секунды |
В совокупности выходит 10+ секунд на один тест. А теперь представьте:
| Количество E2E-тестов | Время выполнения |
|---|---|
| 10 тестов | ~2 минуты |
| 50 тестов | ~10 минут |
| 100 тестов | ~2 минуты |
| 500 тестов | ~1.5 часа |
Что это значит для разработчика:
1. Отправил код → ждешь 20 минут → тесты упали → исправляешь → снова ждешь 20 минут.
2. За день можно сделать 5 – 6 итераций вместо 20 – 30.
3. Продуктивность падает, разработчики начинают ненавидеть тесты.
При работе с E2E тестами в микросервисной архитектуре при изменении в одном сервисе — необходимо запускать все E2E-тесты. Это самая большая проблема. Представьте ситуацию: вы работаете над сервисом Заказов. Вы изменили формат ответа для API /api/orders.
Что происходит:
1. Вы локально протестировали свой сервис (модульные и интеграционные тесты прошли).
2. Вы отправляете код в репозиторий.
3. Запускается CI/CD (конвейер из Лекции 9).
4. Запускаются все E2E-тесты — ведь только они проверяют взаимодействие сервисов.
5. E2E-тесты падают, потому что сервис Платежей не понял новый формат.
6. Вы узнаете об ошибке через 20 – 40 минут.
Ошибка в интеграции между сервисами обнаруживается слишком поздно. А если ошибку нашли через 40 минут — разработчик уже переключился на другую задачу и забыл, что именно менял.
Для решения подобной проблемы, нужен новый вид тестирования. Что нам нужно:
| Требование | Почему |
|---|---|
| Быстрый | Запускается за секунды, а не за минуты |
| Дешевый | Не требует поднятия всех сервисов и браузеров |
| Проверяет совместимость API | Проверяет, что сервисы правильно понимают друг друга |
| Локализует проблему | Показывает, какой именно сервис и какой контракт нарушен |
| Работает в CI/CD | Запускается при каждом push, как модульные тесты |
Этим требованиям соответствует контрактное тестирование.
Давайте сравним разные виды тестирования и посмотрим, какие есть преимущества у контрактного тестирования.
| Вид тестирования | Скорость | Проверяет БД | Проверяет API между сервисами | Проверяет UI | Находит ошибку интеграции |
|---|---|---|---|---|---|
| Модульные | Мгновенно | ❌ | ❌ | ❌ | ❌ |
| Интеграционные | Быстро | ✅ | ❌ (только с БД) | ❌ | ❌ |
| E2E | Медленно | ✅ | ✅ | ✅ | ✅ (но поздно) |
| Контрактные | Быстро | ❌ | ✅ | ❌ | ✅ (рано) |
Что же нам дает контрактное тестирование? До контрактного тестирования у разработчиков была следующая ситуация:
wireframe
Разработчик меняет API → Отправляет код → CI/CD → E2E-тесты (20 мин) → ❌ Ошибка!
С контрактным же тестированием:
wireframe
Разработчик меняет API → Отправляет код → CI/CD → Контрактные тесты (5 сек) → ❌ Ошибка!
В итоге получаем разницу: 20 минут против 5 секунд. Данная разница в процессе разработки является крайне важной и критической.
§10.3. Что такое Контракт и зачем он нужен с точки зрения тестировщика
Давайте еще раз рассмотрим, что такое контракт простыми словами.
Контракт
— это соглашение между двумя сервисами о том, как они будут общаться друг с другом.
В мире микросервисов это звучит следующим образом:
Информация
Сервис А (Потребитель) общается с сервисом Б (Поставщик). Контракт — это правила этого общения.
А что же входит в контракт? Контракт описывает все аспекты взаимодействия между сервисами.
| Что входит в контракт | Что это значит | Пример |
|---|---|---|
| Метод HTTP | Какой метод используется | GET, POST, PUT, DELETE |
| URL (Endpoint) | По какому адресу обращаться | /api/orders, /api/users/{id} |
| Заголовки (Headers) | Какие заголовки должны быть | Content-Type: application/json, Authorization: Bearer <token> |
| Тело запроса (Request Body) | Какие данные отправляются и в каком формате | JSON-объект с полями productId, quantity |
| Тело ответа (Response Body) | Какие данные возвращаются и в каком формате | JSON-объект с полями orderId, status |
| Статус-коды | Какие коды возвращает сервер | 200 OK, 400 Bad Request, 404 Not Found |
Теперь давайте рассмотрим пример того, как выглядит контракт. Представьте, что у нас есть два сервиса:
1. Сервис Заказов (Потребитель) — хочет создать новый заказ.
2. Сервис Платежей (Поставщик) — обрабатывает платежи.
Контракт между ними выглядит так:
| Часть контракта | Что должно быть |
|---|---|
| Метод | POST |
| URL | /api/payments |
| Заголовки | Content-Type: application/json |
| Тело запроса | {"orderId": число, "amount": число, "currency": строка} |
| Успешный ответ | Статус 200 OK, тело {"paymentId": число, "status": "success"} |
| Ошибка | Статус 400 Bad Request, тело {"error": строка} |
Что это значит на практике:
1. Сервис Заказов обязан отправлять POST-запрос на /api/payments с JSON-телом, в котором есть поля orderId, amount, currency.
2. Сервис Платежей обязан принимать такие запросы и отвечать либо 200 OK (если всё хорошо), либо 400 Bad Request (если что-то не так).
Пример реального запроса:
json
POST /api/payments HTTP/1.1
Host: payment-service
Content-Type: application/json
{
"orderId": 12345,
"amount": 100.00,
"currency": "USD"
}
Пример реального ответа:
json
HTTP/1.1 200 OK
Content-Type: application/json
{
"paymentId": 67890,
"status": "success"
}
Контракт
— это как спецификация для тестирования.
Что такое спецификация в тестировании?
Спецификация
— это документ, который описывает, что должна делать система. Тестировщик читает спецификацию и проверяет, соответствует ли система ей. Контракт — это такая же спецификация, но для API.
| Спецификация (для тестировщика) | Контракт (для микросервисов) |
|---|---|
| «Приложение должно авторизовать пользователя» | «Сервис должен принимать POST /api/login с логином и паролем» |
| «Приложение должно показывать список товаров» | «Сервис должен возвращать GET /api/products со списком товаров» |
| «Приложение должно показывать ошибку при неверном вводе» | «Сервис должен возвращать 400 Bad Request при неверных данных» |
Как тестировщик использует контракт:
1. Тестировщик читает контракт: «Сервис Платежей должен принимать POST /api/payments с JSON-телом { orderId, amount, currency }».
2. Тестировщик пишет тест, который проверяет, что:
- Сервис Заказов действительно отправляет такой запрос.
- Сервис Платежей действительно понимает такой запрос и отвечает корректно.
3. Если контракт нарушен — тест падает.
Давайте приведем аналогию в виде технического задания для тестировщика. Представьте, что вы — тестировщик в офисе. Вам дают техническое задание:
wireframe
Сотрудник отдела А должен отправлять заявки на отпуск сотруднику отдела Б. Заявка должна содержать: ФИО, даты отпуска, причину. Сотрудник отдела Б должен подтвердить заявку или вернуть её на доработку.
Вы, как тестировщик, проверяете:
1. Отправляет ли отдел А заявки в правильном формате? (тело запроса)
2. Понимает ли отдел Б эти заявки? (поставщик)
3. Если заявка заполнена неправильно — возвращает ли отдел Б ее на доработку? (ошибка 400)
4. Если заявка правильная — подтверждает ли ее отдел Б? (успех 200)
В мире микросервисов то же самое, но автоматизированное. Важно понять, что контракт — это не код. Это договоренность между командами или сервисами. Что может быть в контракте:
| Формат | Пример | Когда используется |
|---|---|---|
| Устная договоренность | «Сервис А будет отправлять JSON» | В маленьких командах (опасно!) |
| Документ (Word, Google Docs) | Описание API в документе | В средних командах (трудно поддерживать) |
| OpenAPI / Swagger | openapi: 3.0.0 ... | В больших командах (автоматизируется) |
| Pact | Контракт в JSON-файле | Для контрактного тестирования |
§10.4. Контрактное тестирование: новый вид тестирования для микросервисов
Данный параграф хочется начать с повторения.
Контрактное тестирование
— это способ проверки, что два (или более) сервиса правильно понимают друг друга и соблюдают свои договоренности (контракты).
Почему это называется «контрактным» тестированием? Потому что, контракт — это соглашение между сервисами, а контрактное тестирование — это проверка, что обе стороны соблюдают соглашение.
Пример:
| Кто | Что делает | Проверка контракта |
|---|---|---|
| Сервис Заказов (Потребитель) | Отправляет запрос на оплату | Отправляет ли он запрос в правильном формате? |
| Сервис Платежей (Поставщик) | Принимает запрос и обрабатывает | Понимает ли он запрос и правильно ли отвечает? |
Контрактное тестирование проверяет обе стороны:
1. Потребитель действительно отправляет запрос, который ожидает Поставщик.
2. Поставщик действительно умеет обрабатывать такие запросы.
Самая важная идея контрактного тестирования
Потребитель (Consumer) диктует, что ему нужно
Это называется Consumer-Driven Contracts.
Consumer-Driven Contracts
— контракты, управляемые потребителем.
В мире микросервисов это звучит следующим образом:
wireframe
Потребитель (Сервис А) говорит: «Мне нужен API, который принимает `POST /api/payments` с JSON-телом `{ orderId, amount, currency }` и возвращает `200 OK` или `400 Bad Request`».
Поставщик (Сервис Б) обязан:
1. Иметь эндпоинт POST /api/payments.
2. Понимать JSON-тело с полями orderId, amount, currency.
3. Отвечать 200 OK или 400 Bad Request.
Возникает резонный вопрос,
В мире API существует два актора: Producer (поставщик) и Consumer (потребитель). Давайте определим, кто есть кто.
Consumer (Потребитель):
- Сервис, который отправляет запросы.
- Он использует API другого сервиса.
- Он диктует, что ему нужно.
Producer (Поставщик):
- Сервис, который принимает запросы и отвечает на них.
- Он предоставляет API для других сервисов.
- Он должен соответствовать контракту, который диктует потребитель.
Примерная схема взаимодействия двух акторов представлена ниже:

Что контрактное тестирование дает тестировщику? Без контрактного тестирования:
| Действие | Время |
|---|---|
| Изменяем код в одном сервисе | 5 минут |
| Запускаем E2E-тесты | 20 – 40 минут |
| Обнаруживаем, что интеграция сломалась | Поздно |
| Исправляем, снова запускаем E2E | Еще 20 – 40 минут |
В совокупности уходит более одного часа на одну итерацию. С контрактным же тестированием:
| Действие | Время |
|---|---|
| Изменяем код в одном сервисе | 5 минут |
| Запускаем контрактные тесты | 5-10 секунд |
| Обнаруживаем, что интеграция сломалась | Сразу |
| Исправляем, снова запускаем контрактные тесты | 5-10 секунд |
уходит примерно 5 – 10 минут на одну итерацию.
Давайте рассмотрим, что получает тестировщик при использовании контрактного тестирования:
| Что дает контрактное тестирование | Почему это важно |
|---|---|
| Быстрая проверка совместимости | 5 – 10 секунд вместо 20 – 40 минут |
| Локализация проблемы | Точно знает, какой сервис и какой контракт нарушен |
| Раннее обнаружение ошибок | Ошибка найдена до того, как код попал в продакшен |
| Автоматизация | Контрактные тесты запускаются в CI/CD автоматически |
| Документация | Контракты сами по себе являются документацией по API |
В завершение данного параграфа нужно разобрать, чем отличаются E2E – тесты от контрактных тестов:
| Характеристика | E2E-тесты | Контрактные тесты |
|---|---|---|
| Скорость | Медленные (минуты) | Быстрые (секунды) |
| Проверяет UI | Да | Нет |
| Проверяет БД | Да | Нет |
| Проверяет совместимость API | Да | Да |
| Требует поднятия всех сервисов | Да | Нет |
| Находит ошибку интеграции | Поздно | Рано |
| Где запускается | Редко (в main) | Часто (при каждом push) |
Главное их отличие заключается в том, что контрактные тесты проверяют совместимость API быстро и рано. E2E-тесты проверяют всю систему медленно и поздно. Простая аналогия заключается в следующем:
- E2E-тесты — это как генеральная репетиция спектакля. Все актеры на сцене, все костюмы, все декорации. Дорого, долго, но полноценно.
- Контрактные тесты — это как читка текста по ролям. Актеры сидят за столом и читают сценарий. Быстро, дешево, но проверяет только текст (контракт).
§10.5. Инструмент Pact: как автоматизировать контрактное тестирование
Pact
— это самый популярный инструмент для контрактного тестирования. Он позволяет автоматически создавать контракты и проверять, что обе стороны (потребитель и поставщик) соблюдают договоренности.
Pact
— это как нотариус. Он записывает договоренность между двумя сторонами, заверяет её, а потом проверяет, что каждая сторона выполняет свои обязательства.
Pact работает в два этапа: сначала Consumer создает контракт, потом Producer проверяет, что его API соответствует этому контракту. Схема работы примерно следующая:

Давайте подробно разберем эти два этапа.
Этап №1. Consumer пишет тест и записывает контракт
На этом этапе Consumer (потребитель) пишет тест, который описывает, что он ожидает от API поставщика. Это выглядит следующим образом:
1. Consumer пишет тест, в котором описывает:
- Какой запрос он будет отправлять (метод, URL, тело).
- Какой ответ он ожидает получить (статус-код, тело).
2. Pact поднимает Mock-сервер (заглушку), который имитирует поведение поставщика.
3. Consumer отправляет запрос на Mock-сервер.
4. Mock-сервер проверяет, что запрос соответствует описанию, и возвращает ожидаемый ответ.
5. Pact записывает все взаимодействия в контракт — JSON-файл с расширением .pact.
Пример контракта (упрощенно):
json
{
"consumer": { "name": "Сервис Заказов" },
"provider": { "name": "Сервис Платежей" },
"interactions": [
{
"description": "Запрос на оплату заказа",
"request": {
"method": "POST",
"path": "/api/payments",
"body": { "orderId": 123, "amount": 100.00 }
},
"response": {
"status": 200,
"body": { "paymentId": 456, "status": "success" }
}
}
]
}
Что получает тестировщик на этом этапе:
- Контракт в виде JSON-файла.
- Понимание того, какие запросы ожидает Consumer.
- Документацию по API.
Этап №2. Producer проверяет, что его API соответствует контракту
На этом этапе Producer (поставщик) берет контракт от Consumer и проверяет, что его реальный API соответствует этому контракту. Это выглядит следующим образом:
1. Producer получает контракт (JSON-файл) от Consumer.
2. Pact запускает проверку:
- Берет каждый запрос из контракта.
- Отправляет его на реальный API Producer.
- Сравнивает реальный ответ с ожидаемым из контракта.
3. Если все запросы дали ожидаемые ответы — проверка пройдена.
4. Если хотя бы один ответ не совпадает — проверка провалена.
Что получает тестировщик на этом этапе:
- Отчет о совместимости сервисов.
- Точное указание, какой именно контракт нарушен (если есть проблема).
- Гарантию, что Producer совместим с Consumer.
А что же дает Pact тестировщику? На самом деле, он дает много преимуществ:
| Что дает Pact | Почему это важно |
|---|---|
| Автоматический отчет о совместимости | Не нужно вручную проверять, что API совместимы |
| Быстрая обратная связь | Проверка занимает секунды, а не минуты |
| Локализация проблемы | Точно знает, какой сервис и какой контракт нарушен |
| Документация API | Контракты сами по себе являются документацией |
| Интеграция с CI/CD | Контракты проверяются при каждом push |
Самое важное преимущество контрактного тестирования – это безопасность, то есть, сервисы развиваются независимо.
Без Pact:
wireframe
Изменение в Producer → Запуск всех E2E-тестов → Ошибка через 40 минут
С Pact:
wireframe
Изменение в Producer → Запуск контрактных тестов → Ошибка через 5 секунд
Что это дает:
- Сервисы могут развиваться независимо. Producer может менять свой код, если контракты не нарушаются.
- Ошибки находятся рано. Разработчик узнает о проблеме сразу, а не через 40 минут.
- Меньше стресса. Можно безопасно обновлять один сервис, не боясь сломать другие.
Контракты можно хранить в двух местах:
1. Локально (для начала):
wireframe
/pacts
└── consumer-provider.json
2. Pact Broker (для CI/CD):
Pact Broker
— это сервер для хранения и управления контрактами.
Он позволяет:
- Хранить все контракты в одном месте.
- Отслеживать версии контрактов.
- Проверять, можно ли безопасно развернуть сервис.
§10.6. Контрактное тестирование в CI/CD: автоматическая проверка совместимости
В §9.5 из лекции №9 мы научились настраивать CI/CD-конвейер, который автоматически запускает модульные, интеграционные и E2E-тесты при каждом git push. Теперь мы добавим в этот конвейер контрактные тесты.
Главная идея этого действия заключается в том, что контрактное тестирование должно быть встроено в CI/CD так же, как и другие виды тестирования. Это единственный способ получить быструю обратную связь о совместимости сервисов.
В CI/CD то же самое: если контракт нарушен — конвейер останавливается, и разработчик получает уведомление.
Контрактное тестирование встраивается в CI/CD в два этапа, которые соответствуют двум сторонам контракта:
Этап №1. Consumer создает контракт
| Шаг | Что происходит |
|---|---|
| 1 | Разработчик отправляет код в репозиторий (git push) |
| 2 | Запускается CI/CD-конвейер |
| 3 | Конвейер запускает тесты Consumer |
| 4 | Pact записывает контракт в JSON-файл |
| 5 | Конвейер публикует контракт в Pact Broker (общее хранилище) |
Этап №2. Producer проверяет контракт
| Шаг | Что происходит |
|---|---|
| 1 | Разработчик отправляет код Producer |
| 2 | Запускается CI/CD-конвейер Producer |
| 3 | Конвейер скачивает контракт из Pact Broker |
| 4 | Конвейер запускает проверку Producer |
| 5 | Pact проверяет, что API Producer соответствует контракту |
| 6 | Если проверка пройдена — конвейер зеленый |
| 7 | Если проверка провалена — конвейер красный |
Схема работы контрактов в CI/CD выглядит следующим образом:

Что получает команда при работе с контрактным тестированием:
| Что | Когда | Почему важно |
|---|---|---|
| Мгновенное уведомление | Через 5 – 10 секунд после git push | Разработчик еще помнит, что менял |
| Точное указание проблемы | Сразу в отчете | «Контракт нарушен: поле amount ожидается как число, пришла строка» |
| Красный конвейер | Немедленно | Нельзя проигнорировать проблему |
| Блокировка релиза | Автоматически | Пока контракт не исправлен — релиз невозможен |
Главное правило при интеграции контрактного тестирования в CI/CD заключается в том, что конвейер должен падать, если контракт нарушен.
Это единственный способ гарантировать, что несовместимые сервисы не попадут в продакшен.
Как это выглядит в CI/CD (GitHub Actions):
yaml
name: Contract Tests
on:
push:
branches: [ main ]
jobs:
consumer-tests:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0'
- name: Run consumer tests
run: dotnet test --filter "Category=Contract"
- name: Publish contract to Pact Broker
run: |
# Публикуем контракт в Pact Broker
# Если контракт не опубликован — конвейер падает
provider-verification:
runs-on: ubuntu-latest
needs: consumer-tests
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0'
- name: Run provider verification
run: |
# Скачиваем контракт из Pact Broker
# Проверяем, что API соответствует контракту
# Если проверка провалена — конвейер падает
А для разработчиков это обычно проявляется в двух сценариях.
Сценарий №1. Всё хорошо
1. Разработчик меняет код в Consumer и отправляет git push.
2. Конвейер запускается → зеленый.
3. Контракт публикуется в Pact Broker.
4. Producer автоматически проверяет контракт → зеленый.
5. Разработчик спокоен: «Всё совместимо».
Сценарий №2. Контракт нарушен
1. Разработчик меняет код в Producer (изменил формат ответа) и отправляет git push.
2. Конвейер Producer запускается.
3. Контрактные тесты провалены.
4. Разработчик видит красный значок и отчет: «Ошибка: ожидалось поле 'amount', пришло поле 'total'».
5. Разработчик исправляет код и отправляет снова.
6. Конвейер зеленый.
Время на обнаружение ошибки варьируется в диапазоне от 5 – 10 секунд.
Самая частая ситуация в микросервисной архитектуре это когда Consumer и Producer находятся в разных репозиториях. У каждого сервиса свой репозиторий.
Как это работает:
1. Consumer публикует контракт в Pact Broker при каждом push.
2. Producer скачивает контракт из Pact Broker при каждом push и проверяет свой API.
Схема данного взаимодействия представлена ниже:

§10.7. Антипаттерны в контрактном тестировании или его делать НЕ надо
Контрактное тестирование — это мощный инструмент, но только если вы используете его правильно.
Есть несколько типичных ошибок, которые сводят на нет всю пользу от контрактного тестирования.
Главная мысль этого параграфа заключается в том, что контрактное тестирование — это процесс, который требует внимания к деталям и постоянного обновления.
Давайте подробно разберем антипаттерны, которые часто встречаются при работе с контрактным тестированием.
Антипаттерн №1. «Проверяем только успешные сценарии»
Суть проблемы заключается в том, что разработчик пишет контрактные тесты только для «счастливого пути» (Happy Path) — когда всё работает правильно. Но он забывает про ошибки и исключения. Пример (как НЕ делать):
json
{
"consumer": "Сервис Заказов",
"provider": "Сервис Платежей",
"interactions": [
{
"description": "Успешная оплата",
"request": {
"method": "POST",
"path": "/api/payments",
"body": { "orderId": 123, "amount": 100.00 }
},
"response": {
"status": 200,
"body": { "paymentId": 456, "status": "success" }
}
}
]
}
Почему это плохо? В реальной жизни сервис Платежей может вернуть ошибку:
- Недостаточно средств.
- Неверный номер карты.
- Сервис временно недоступен.
- Превышен лимит операций.
Если Consumer не умеет обрабатывать эти ошибки — система упадет в продакшене.
Что нужно делать правильно:
json
{
"consumer": "Сервис Заказов",
"provider": "Сервис Платежей",
"interactions": [
{
"description": "Успешная оплата",
"request": {
"method": "POST",
"path": "/api/payments",
"body": { "orderId": 123, "amount": 100.00 }
},
"response": {
"status": 200,
"body": { "paymentId": 456, "status": "success" }
}
},
{
"description": "Недостаточно средств",
"request": {
"method": "POST",
"path": "/api/payments",
"body": { "orderId": 123, "amount": 10000.00 }
},
"response": {
"status": 400,
"body": { "error": "Недостаточно средств" }
}
},
{
"description": "Сервис недоступен",
"request": {
"method": "POST",
"path": "/api/payments"
},
"response": {
"status": 503,
"body": { "error": "Сервис временно недоступен" }
}
}
]
}
Антипаттерн №2. «Игнорируем старые версии»
Суть проблемы заключается в том, что в микросервисной архитектуре сервисы обновляются с разной скоростью. Producer обновил API, но Consumer еще не обновился и использует старую версию. Пример:
wireframe
Версия API: v1
Producer: /api/payments (старый формат)
Consumer: /api/payments (старый формат)
✅ Всё работает
Через неделю:
Producer: /api/v2/payments (новый формат)
Consumer: /api/payments (старый формат)
❌ Контракт нарушен! Consumer не может найти старый формат.
Почему это плохо:
| Что произошло | Последствия |
|---|---|
| Producer изменил API, но Consumer не обновился | Consumer падает при каждом запросе |
| Producer удалил старый эндпоинт | Пользователи не могут оплатить заказы |
| Producer переименовал поле | Consumer не понимает новые данные |
Что нужно делать правильно:
1. Поддерживать обратную совместимость:
json
{
"consumer": "Сервис Заказов",
"provider": "Сервис Платежей",
"interactions": [
{
"description": "Старая версия API (v1)",
"request": {
"method": "POST",
"path": "/api/payments",
"body": { "orderId": 123, "amount": 100.00 }
},
"response": {
"status": 200,
"body": { "paymentId": 456, "status": "success" }
}
},
{
"description": "Новая версия API (v2)",
"request": {
"method": "POST",
"path": "/api/v2/payments",
"body": { "orderId": 123, "amount": 100.00, "currency": "USD" }
},
"response": {
"status": 200,
"body": { "paymentId": 456, "status": "success" }
}
}
]
}
2. Использовать версионирование API:
- Не удалять старые эндпоинты сразу.
- Дать время Consumer перейти на новую версию.
- Удалять старую версию только когда все Consumer обновились.
Антипаттерн №3. «Контракт один раз и забыли»
Суть проблемы заключается в том, что контракт был написан один раз в начале проекта и с тех пор не обновлялся. API изменилось, но контракт остался старым. Пример:
wireframe
Месяц 1: Контракт создан ✅
Месяц 3: API изменилось, но контракт не обновлен ⚠️
Месяц 6: API сильно изменилось, контракт устарел ❌
Почему это плохо:
| Что произошло | Последствия |
|---|---|
| Контракт устарел | Он больше не отражает реальное поведение API |
| Тесты проходят, но API не работает | Ложное чувство уверенности |
| Новый разработчик смотрит контракт | Получает неверную информацию об API |
Что нужно делать правильно:
1. Контракт обновляется при каждом изменении API.
Если вы изменили API — вы должны изменить контракт.
2. Контракт — это живые документ.
Он должен быть частью процесса разработки, а не артефактом из прошлого.
3. Контрактные тесты запускаются при каждом push.
Если кто-то изменил API и забыл обновить контракт — тесты упадут и напомнят об этом.
Антипаттерн №4. «Всё через E2E»
Суть проблемы заключается в том, что команда решает, что контрактное тестирование не нужно, потому что есть E2E-тесты. Они проверяют всё, значит, достаточно. Почему это плохо:
| E2E-тесты | Контрактные тесты |
|---|---|
| Медленные (минуты) | Быстрые (секунды) |
| Дорогие (поднимают все сервисы) | Дешевые (проверяют только API) |
| Сложно отлаживать | Легко локализовать проблему |
| Не работают в CI/CD часто | Работают при каждом push |
Контрактное тестирование НЕ заменяет E2E. Они решают разные задачи:
| Что проверяет | Контрактные тесты | E2E-тесты |
|---|---|---|
| Совместимость API | ✅ | ✅ |
| Правильность работы UI | ❌ | ✅ |
| Правильность работы БД | ❌ | ✅ |
| Полный пользовательский сценарий | ❌ | ✅ |
| Скорость выполнения | Быстро | Медленно |
Что нужно делать правильно:
| Уровень | Что использовать | Как часто |
|---|---|---|
| Быстрая проверка | Контрактные тесты | При каждом push |
| Полная проверка | E2E-тесты | В main или перед релизом |
Можно привести простую аналогию при работе с контрактными тестами и E2E – тестами:
- Контрактные тесты — это как быстрая проверка зубов у стоматолога. Занимает 5 минут, но показывает основные проблемы.
- E2E-тесты — это как полное МРТ-обследование. Занимает час, но показывает всё.
Важно понимать, что нужно и то, и другое.