Контрактное тестирование — Примеры
Пример 1 — Ответы, на которые тест никогда не смотрел
Тест проверяет, что создание корзины отвечает 201 и содержит id, — и больше ничего. Сервис отвечает ещё и на дублирующую отправку и на плохое тело, а поле однажды добавили в успешный ответ, не меняя документ, — тест всё равно прошёл; расширьте проверку так, чтобы каждый описанный ответ сверялся с документом и чтобы незаявленное поле обнаруживалось.
test("создание корзины", async () => {
const res = await request(app).post("/carts").send({ items: [] });
expect(res.status).toBe(201);
expect(res.body.id).toBeDefined();
});
Решение
const document = loadDocument("openapi.yaml");
test.each([
["201", { body: { items: [] } }],
["409", { body: { items: [] }, headers: { "Idempotency-Key": "cart-1" } }],
["422", { body: { items: "many" } }],
])("создание отвечает на каждый описанный ответ: %s", async (status, req) => {
const res = await request(app).post("/carts").send(req.body).set(req.headers ?? {});
expect(res.status).toBe(Number(status));
const schema = document.paths["/carts"].post.responses[status].content["application/json"].schema;
expect(validate(schema, res.body)).toEqual({ valid: true, unexpected: [] });
});
Теперь задействован каждый код состояния из документа, поэтому переименованное поле, расширенный тип или изменённый код валят сборку, а не доходят до клиента; валидатор настроен сообщать о неизвестных полях, и именно это превращает документ в инструмент затягивания гаек. Добавленное поле discount появляется в unexpected в тот момент, когда его отправляют. А частая ошибка — проверять только успешный ответ и позволять неизвестным полям проходить, из-за чего форма ошибки вольна расходиться незаметно.
Пример 2 — Требование клиента, проверяемое производителем
Страница оформления заказа читает два поля из корзины и сломалась бы, исчезни любое из них, а файл ниже — то, что этот клиент опубликовал как своё требование. Добавьте проверку, которую выполняет сборка производителя, чтобы удаление нужного клиенту поля проваливало сборку до выпуска изменения.
{
"consumer": "checkout-page",
"expects": [
{ "method": "get", "path": "/carts/42", "status": 200, "requiredFields": ["id", "items"] }
]
}
Решение
for (const entry of contract.expects) {
const res = await request(app)[entry.method](entry.path);
expect(res.status).toBe(entry.status);
for (const field of entry.requiredFields) {
expect(res.body).toHaveProperty(field);
}
}
Производитель, который проверяет собственные ответы, доказывает соответствие API его описанию, а не то, что его клиенты по-прежнему работают; проверка же опубликованного требования ловит удаление в сборке производителя, а не в следующем релизе клиента. Поскольку проверка касается форм, а не поведения, она достаточно дешева для каждого запроса на слияние — а там расхождение находится до слияния. Однако там, где удаление намеренно, версия поднимается, а старый контракт остаётся соблюдённым, пока не закроется окно устаревания.