Тестирование 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 — быстрый барьер, отсекающий мёртвые релизы сразу после деплоя.