---
course: ПиТПМ
lecture: 10
title: "Лекция №10. Архитектура микросервисов и стратегии их поддержки. Контрактное тестирование."
---

# Лекция №10. Архитектура микросервисов и стратегии их поддержки. Контрактное тестирование.

## §10.1. От монолита к микросервисам: как изменилось тестирование

В курсе **«Основы Алгоритмизации и Программирования»** вы уже знакомились с понятиями **монолитной** и **микросервисной архитектуры**.

Давайте коротко освежим память, потому что это напрямую влияет на то, как мы **тестируем системы**.

**Монолитная архитектура**, это когда **вся система — это одно приложение**. Все **компоненты** (*интерфейс, бизнес-логика, работа с базой данных*) находятся внутри **одного проекта**, собираются в один **исполняемый файл** и **развертываются как одно целое**.

Ключевая особенность данной архитектуры

Все компоненты внутри одного приложения. Если один компонент падает — падает вся система.

Схема монолитной архитектуры представлена ниже:

![](/images/lectures/pitpm/10/image-01.webp)

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

Ключевая особенность данной архитектуры

Каждый микросервис — это отдельное приложение. Если один микросервис падает — остальные продолжают работать.

Схема микросервисной архитектуры представлена ниже:

![](/images/lectures/pitpm/10/image-02.webp)

Для наглядности, давайте сравним эти две архитектуры в виде таблицы:

| Характеристика | Монолит | Микросервисы |
| --- | --- | --- |
| **Структура** | Одно приложение | Много маленьких приложений |
| **База данных** | Одна БД для всех компонентов | Каждый сервис — своя БД |
| **Развертывание** | Всё приложение целиком | Каждый сервис независимо |
| **Технологии** | Один язык/стек для всего | Разные языки для разных сервисов |
| **Масштабирование** | Масштабируется целиком | Масштабируется только нужные сервисы |

Переход **от монолита к микросервисам** кардинально меняет то, как мы тестируем систему.

В монолите всё тестируется вместе. То есть выглядит это следующим образом:

`1.` Вы запускаете приложение (одно).

`2.` Оно поднимает базу данных.

`3.` Вы запускаете тесты.

`4.` Тесты проверяют всё: и UI, и логику, и работу с БД.

`5.` Всё это происходит в одном процессе.

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

- **Просто.** Одно приложение — один набор тестов.
- **Быстро.** Нет сетевых задержек между компонентами.
- **Предсказуемо.** Все компоненты всегда вместе.

но у такого вида есть и недостатки:

- Если тесты упали — непонятно, какой компонент сломался.
- Невозможно протестировать компонент отдельно.
- Любое изменение требует полного регрессионного тестирования.

В микросервисах же всё тестируется по отдельности. Как это выглядит:

`1.` Каждый микросервис тестируется независимо (модульные тесты).

`2.` Проверяется, что микросервис правильно работает со своей БД (интеграционные тесты).

`3.` Проверяется, что микросервис правильно общается с другими микросервисами (НОВАЯ ЗАДАЧА!).

`4.` Проверяется, что вся система работает вместе (E2E-тесты).

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

- **Изолированность.** Можно тестировать каждый сервис отдельно.
- **Скорость.** Тесты одного сервиса не зависят от других.
- **Гибкость.** Можно менять один сервис, не перетестируя все остальные.

но у такого вида есть и недостатки:

- **Сложность.** Нужно тестировать не только каждый сервис, но и их взаимодействие.
- **Сеть.** Появляются сетевые задержки и ошибки, которых нет в монолите.
- **Версии.** Сервисы могут развиваться с разной скоростью.

А как проверить, что сервисы правильно понимают друг друга? В **монолите** эта **проблема не стоит**. Все компоненты внутри одного приложения общаются через вызовы методов. Компилятор сам проверяет, что типы данных совпадают. В **монолите** это происходит все **следующим образом**:

csharp

```csharp
// Всё в одном приложении
var order = new Order { 
    Id = 1, 
    Total = 100 
};

// Компилятор проверит, что метод существует
orderService.Create(order);
```

В **микросервисах** же **ситуация состоит иначе**:

csharp

```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

```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

```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

```json
POST /api/payments HTTP/1.1
Host: payment-service
Content-Type: application/json

{
  "orderId": 12345,
  "amount": 100.00,
  "currency": "USD"
}
```

Пример реального ответа:

json

```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 для других сервисов.
- Он должен соответствовать контракту, который диктует потребитель.

Примерная схема взаимодействия двух акторов представлена ниже:

![](/images/lectures/pitpm/10/image-03.webp)

Что **контрактное тестирование** дает **тестировщику**? Без **контрактного тестирования**:

| Действие | Время |
| --- | --- |
| **Изменяем код в одном сервисе** | 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** соответствует этому контракту. Схема работы примерно следующая:

![](/images/lectures/pitpm/10/image-04.webp)

Давайте подробно разберем эти два этапа.

**Этап №1. Consumer пишет тест и записывает контракт**

На этом этапе **Consumer (потребитель)** пишет тест, который описывает, что он ожидает от **API** поставщика. Это выглядит следующим образом:

`1.` Consumer пишет тест, в котором описывает:

- Какой запрос он будет отправлять (метод, URL, тело).
- Какой ответ он ожидает получить (статус-код, тело).

`2.` Pact поднимает Mock-сервер (заглушку), который имитирует поведение поставщика.

`3.` Consumer отправляет запрос на Mock-сервер.

`4.` Mock-сервер проверяет, что запрос соответствует описанию, и возвращает ожидаемый ответ.

`5.` Pact записывает все взаимодействия в контракт — JSON-файл с расширением `.pact`.

Пример контракта (упрощенно):

json

```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 выглядит следующим образом:

![](/images/lectures/pitpm/10/image-05.webp)

Что получает команда при работе с **контрактным тестированием**:

| Что | Когда | Почему важно |
| --- | --- | --- |
| **Мгновенное уведомление** | Через 5 – 10 секунд после `git push` | Разработчик еще помнит, что менял |
| **Точное указание проблемы** | Сразу в отчете | «Контракт нарушен: поле `amount` ожидается как число, пришла строка» |
| **Красный конвейер** | Немедленно | Нельзя проигнорировать проблему |
| **Блокировка релиза** | Автоматически | Пока контракт не исправлен — релиз невозможен |

**Главное правило** при интеграции **контрактного тестирования** в **CI/CD** заключается в том, что конвейер должен падать, если контракт нарушен.

Это единственный способ гарантировать, что несовместимые сервисы не попадут в продакшен.

Как это выглядит в **CI/CD (GitHub Actions)**:

yaml

```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.

Схема данного взаимодействия представлена ниже:

![](/images/lectures/pitpm/10/image-06.webp)

## §10.7. Антипаттерны в контрактном тестировании или его делать НЕ надо

К**онтрактное тестирование** — это мощный инструмент, но только если вы используете его правильно.

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

Главная мысль этого параграфа заключается в том, что **контрактное тестирование** — *это процесс, который требует внимания к деталям и постоянного обновления*.

Давайте подробно разберем **антипаттерны**, которые часто встречаются при работе с **контрактным тестированием**.

**Антипаттерн №1. «Проверяем только успешные сценарии»**

Суть проблемы заключается в том, что разработчик пишет **контрактные тесты** только для **«счастливого пути»** (*Happy Path*) — когда всё работает правильно. Но он забывает про ошибки и исключения. Пример (*как НЕ делать*):

json

```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

```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

```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-тесты** — это как полное МРТ-обследование. Занимает час, но показывает всё.

Важно понимать, что нужно и то, и другое.
