Модуль 2 · Автоматизация · урок 7 из 13
API-тесты и контракты
API-тесты дешевле и стабильнее e2e, а для мобильных приложений они ещё и единственная страховка от того, что бэкенд тихо поменяет формат ответа. Агенту здесь удобно: запрос и ответ — текст, вход и выход однозначны. Разберём, откуда агенту брать ожидания, как писать тесты на Playwright и что такое контрактная проверка на практике.
Источник истины
Первый вопрос до любого кода: по чему агент будет строить ожидания?
- Спецификация API (OpenAPI или описание в документации проекта) — лучший вариант. Тесты сверяют поведение с договорённостью.
- Требования из задачи — если спецификации нет.
- Код контроллеров и правила валидации — удобно, но опасно. Как и в тест-дизайне, тесты по коду подтверждают то, что код делает, а не то, что должен.
Рабочий компромисс: попросить агента восстановить описание API из кода, отдать его на сверку разработчику или аналитику и только потом писать тесты по утверждённому описанию.
Прочитай маршруты и контроллеры плагина src/plugins/<vendor>/<plugin>
(routes.php, controllers, правила валидации). Составь описание API
в виде таблицы: метод, путь, авторизация, поля запроса с типами и
правилами, коды ответов, поля ответа. Где поведение неочевидно —
пометь «уточнить». Тесты пока не пиши.Это описание полезно само по себе: часто его нет нигде, кроме кода.
Тесты на Playwright
Если e2e уже на Playwright, API-тесты удобно держать там же: фикстура request даёт HTTP-клиент с тем же baseURL.
import { test, expect } from '@playwright/test';
test.describe('POST /api/leads', () => {
test('отклоняет заявку без email и телефона', async ({ request }) => {
const res = await request.post('/api/leads', {
data: { name: 'QA', email: '', phone: '' },
});
expect(res.status()).toBe(422);
const body = await res.json();
expect(body.errors).toHaveProperty('email');
});
test('не принимает GET', async ({ request }) => {
const res = await request.get('/api/leads');
expect(res.status()).toBe(405);
});
});Код 422 с полем errors — стандартный ответ валидации Laravel; если ваш API отвечает иначе, это и должно быть в описании API, а не в догадках агента.
Матрица негативных сценариев
Попросите агента пройти по каждому эндпоинту с одним и тем же списком измерений:
- обязательные поля отсутствуют, пустые,
null, неверного типа; - границы длины и формата, юникод, очень большие тела запроса;
- неверный метод, неверный
Content-Type, битый JSON; - без авторизации, с чужим токеном, с просроченным токеном;
- доступ к чужому ресурсу по подмене идентификатора (
/api/orders/124вместо своего123); - повторная отправка того же запроса — не создаёт ли дубль.
Пункт про чужой ресурс особенно важен: ошибки контроля доступа агент сам ищет редко, а в API для мобильных приложений они встречаются часто.
Контракты: форма ответа
Мобильное приложение в сторе нельзя обновить мгновенно. Если бэкенд переименовал поле или сделал его необязательным, старые версии приложения сломаются у пользователей. Контрактная проверка ловит это до релиза: тест сверяет не конкретные значения, а форму ответа.
Простой вариант — схема ответа в тесте. Ниже пример с библиотекой zod; подойдёт любая библиотека схем, которая уже есть в проекте.
import { test, expect } from '@playwright/test';
import { z } from 'zod';
const CaseItem = z.object({
id: z.number().int(),
slug: z.string().min(1),
title: z.string().min(1),
cover: z.string().url().nullable(),
tags: z.array(z.string()),
});
test('GET /api/cases соответствует контракту мобильного клиента', async ({ request }) => {
const res = await request.get('/api/cases?limit=5');
expect(res.status()).toBe(200);
const parsed = z.array(CaseItem).safeParse(await res.json());
expect(parsed.success, JSON.stringify(parsed.error?.issues, null, 2)).toBe(true);
});Сообщение во втором аргументе expect выводит список расхождений — по нему сразу видно, какое поле поменялось. Попросите агента собрать такие схемы для всех эндпоинтов, которые использует мобильное приложение, причём по модели данных в коде приложения, а не по ответу сервера. Тогда тест проверяет ожидания клиента, а не фиксирует текущее поведение бэкенда.
В зрелом варианте это контракты, которые ведёт сторона-потребитель (consumer-driven contracts), со своими инструментами. Начинать можно со схем в тестах — это уже закрывает основной риск.
Чего агенту не разрешать
- Ходить в боевой API с запросами на запись. Только локальный стенд или тестовое окружение. Запишите это в AGENTS.md и не включайте агенту сеть шире нужного.
- Сравнивать ответ целиком со снимком. В ответе есть даты, идентификаторы, порядок — такой тест падает на каждом прогоне, и агент начнёт «чинить» его, ослабляя проверки.
- Ограничиваться кодом ответа. Тест, который проверяет только
200, пройдёт и тогда, когда в теле пусто. - Дёргать настоящие внешние сервисы — CRM, почту, платёжный шлюз. В тестах они подменяются.
Ревью API-тестов
Перед тем как принять работу, задайте агенту контрольный вопрос — его стоит повторять для любого набора тестов:
Для каждого теста в tests/api/leads.spec.ts назови одно конкретное
изменение в коде API, при котором тест упадёт. Если такого изменения
не придумать — тест бесполезен, пометь его.Это упрощённая ручная версия мутационного тестирования, о котором подробно — в уроке «Как проверять тесты, написанные агентом».
Попробуйте сами
10–15 мин на рабочем месте- Попросите агента восстановить описание API одного модуля из кода и отметить места «уточнить». Покажите таблицу разработчику.
- Напишите вместе с агентом три негативных теста по матрице из урока, включая доступ к чужому ресурсу.
- Если у вас есть мобильное приложение, сделайте схему ответа для одного эндпоинта по модели данных в коде клиента и прогоните тест.
Коротко
- Ожидания — из спецификации или утверждённого описания API, а не напрямую из кода контроллеров.
- Фикстура
requestв Playwright позволяет держать API-тесты рядом с e2e. - Негативная матрица одинакова для всех эндпоинтов; доступ к чужому ресурсу проверяйте всегда.
- Контрактная проверка формы ответа защищает мобильные клиенты от молчаливых изменений бэкенда.
- Только тестовые окружения, никаких снимков ответа целиком и проверок одного лишь статуса.