Тестирование API: Postman и контрактные тесты

Почему «вроде работает в браузере» — это не проверка, и как сделать так, чтобы API не ломался молча.

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

API живёт долго. За это время меняется код, базы, зависимости. Без тестов любое изменение — рулетка: кто-то переименовал поле в ответе, и три клиента молча сломались. Хорошие тесты ловят это до того, как сломается прод. В этом уроке мы пройдём путь от ручной проверки руками до контрактных тестов, которые гарантируют, что потребитель и провайдер API не разойдутся.

Ручное тестирование: Postman и Insomnia

Первый инструмент любого разработчика API — Postman (или его аналог Insomnia). Это GUI, где можно собрать запрос: выбрать метод, указать URL, добавить заголовки и тело, отправить и увидеть ответ. Звучит как «браузер для не-GET-запросов», но сила в трёх вещах.

Коллекции — это сохранённые наборы запросов, сгруппированные по сущностям: «создать заказ», «получить заказ», «отменить». Коллекцию можно передать коллеге или приложить к документации. Окружения (environments) — наборы переменных: {{baseUrl}}, {{token}}. Переключив окружение с dev на staging, вы прогоняете те же запросы по другому серверу, не переписывая URL. Это превращает Postman из игрушки в инструмент регрессии.

POST {{baseUrl}}/orders
Authorization: Bearer {{token}}
Content-Type: application/json

Автотесты эндпоинтов

Ручная проверка не масштабируется: никто не будет кликать сто запросов после каждого коммита. Поэтому пишут автотесты. Минимальный автотест эндпоинта проверяет три вещи: статус-код, схему ответа (есть ли нужные поля и тех ли они типов) и значения ключевых полей.

В Postman сценарии пишут на JavaScript во вкладке Tests. Идея универсальна и читается даже без знания фреймворка:

// Проверки после ответа на GET /orders/42
pm.test("Статус 200", () => {
  pm.response.to.have.status(200);
});

pm.test("Тело — JSON с нужными полями", () => {
  const body = pm.response.json();
  pm.expect(body).to.have.property("id");
  pm.expect(body).to.have.property("status");
  pm.expect(body.total).to.be.a("number");
});

Здесь три ассерта: статус равен 200, в теле есть поля id и status, а total — число. Если бэкенд переименует total в amount, тест упадёт сразу, а не у клиента в продакшене.

Проверка схемы целиком

Поле за полем проверять утомительно. Лучше — сверить весь ответ с JSON Schema (той самой, что лежит в OpenAPI-спеке). Логика ассерта в виде данных:

{
  "type": "object",
  "required": ["id", "status", "total"],
  "properties": {
    "id":     { "type": "integer" },
    "status": { "type": "string", "enum": ["new", "paid", "shipped"] },
    "total":  { "type": "number" }
  }
}

Тест берёт реальный ответ и валидирует его против этой схемы. Один ассерт покрывает структуру целиком — и автоматически синхронизируется с документацией.

Контрактные тесты

Теперь главная идея урока. Представьте: фронтенд (потребитель, consumer) обращается к бэкенду (поставщик, provider). Бэкенд меняет ответ, его собственные тесты зелёные — но фронтенд сломан, потому что ждал старое поле. Эту пропасть закрывают контрактные тесты.

Contract testing — проверка, что потребитель и поставщик одинаково понимают формат обмена. Контракт — это формальное описание того, какие запросы шлёт потребитель и какие ответы ему обязан вернуть поставщик.

Подход consumer-driven (за ним стоит идея инструмента Pact): контракт пишет потребитель. Фронтенд заявляет: «когда я делаю GET /orders/42, я ожидаю объект с полями id, status, total». Этот контракт сохраняется в файл и передаётся на сторону поставщика. Поставщик в своём CI прогоняет контракт против реального API: если он перестал отдавать total — сборка падает у поставщика, ещё до релиза. Так рассинхрон ловится в момент изменения, а не в проде у клиента.

{
  "consumer": "web-frontend",
  "provider": "orders-api",
  "interaction": {
    "request":  { "method": "GET", "path": "/orders/42" },
    "response": {
      "status": 200,
      "body":   { "id": 42, "status": "paid", "total": 1990 }
    }
  }
}

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

Тесты на ошибки и edge cases

Happy path тестировать легко и потому соблазнительно. Но баги живут в углах. Проверяйте: запрос несуществующего ресурса (404), невалидное тело (400 с понятной ошибкой), отсутствие или истёкший токен (401), доступ к чужому ресурсу (403), пустые и граничные значения (пустой список, 0, отрицательные числа, очень длинные строки), повторную отправку (идемпотентность PUT, дубликат при POST).

СценарийОжидаемый статус
Ресурс не существует404
Невалидное тело запроса400
Нет/истёк токен401
Чужой ресурс403
Конфликт состояния409

Smoke-тесты в CI

Smoke-тест — это минимальный набор проверок «дым пошёл или нет»: жив ли сервис вообще. Обычно это пара запросов к /health и одному-двум ключевым эндпоинтам сразу после деплоя. Их встраивают в CI/CD: пайплайн собрал образ, поднял сервис, прогнал smoke — если упало, откатываемся, не пуская кривой релиз к пользователям. Полные тесты могут идти минуты, smoke — секунды, поэтому он стоит первым барьером.

Как работает под капотом

Любой API-тест — это HTTP-клиент плюс ассерты. Клиент сериализует запрос (метод, путь, заголовки, тело), отправляет по сети, получает сырой ответ: статус-строку, заголовки, байты тела. Фреймворк парсит тело (обычно JSON) в структуру данных, и дальше работают ассерты — обычные проверки равенства и наличия. Контрактный тест устроен хитрее: на стороне потребителя он поднимает мок-поставщика, отдающего ответы из контракта, и проверяет, что код потребителя их переваривает. На стороне поставщика тот же контракт играет роль набора ожиданий, прогоняемых против настоящего сервиса. Контракт-файл — общий артефакт, который ездит между командами (часто через брокер).

Частые ошибки

  • Только happy path. Тесты на 200 есть, на 400/401/404 — нет. Реальные баги именно там.
  • Проверка тела «как строки». Сравнение всего JSON посимвольно ломается от перестановки полей. Сверяйте по схеме и значениям ключей.
  • Тесты, зависящие от состояния БД. Тест проходит первый раз и падает второй, потому что заказ уже создан. Делайте тесты идемпотентными или чистите данные.
  • Контракт пишет поставщик. В consumer-driven контракт диктует потребитель — иначе теряется смысл: поставщик угадывает, а не слышит реальные ожидания.
  • Нет smoke в CI. Полный прогон долгий, его выключают на хотфиксах — и кривой релиз уходит в прод. Smoke должен быть всегда.

Итоги

  • Postman/Insomnia с коллекциями и окружениями — основа ручной и полуавтоматической проверки.
  • Автотест эндпоинта проверяет статус, схему ответа и значения ключевых полей.
  • Контрактные тесты (идея Pact, consumer-driven) гарантируют, что потребитель и поставщик не разойдутся.
  • Edge cases и ошибки (400/401/403/404/409) важнее happy path — там живут баги.
  • Smoke-тесты в CI/CD — быстрый барьер, отсекающий мёртвые релизы сразу после деплоя.
Проверьте себя
1. В чём суть consumer-driven контрактного тестирования?
AПоставщик описывает весь свой API, а потребитель подстраивается
BПотребитель описывает свои ожидания к ответам, и поставщик проверяет их в своём CI
CТестируется только скорость ответа
DКонтракт пишется вручную после каждого релиза без автоматизации
2. Что должен проверять минимальный автотест REST-эндпоинта?
AТолько то, что сервер ответил хоть что-то
BСтатус-код, структуру (схему) ответа и значения ключевых полей
CТолько время ответа
DТолько заголовок Content-Type
3. Зачем нужны smoke-тесты в CI/CD?
AЧтобы заменить все остальные тесты
BЧтобы быстро убедиться, что сервис жив сразу после деплоя, и откатить кривой релиз
CЧтобы измерить покрытие кода
DЧтобы документировать API