Контрактное тестирование — Примеры

Пример 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 его описанию, а не то, что его клиенты по-прежнему работают; проверка же опубликованного требования ловит удаление в сборке производителя, а не в следующем релизе клиента. Поскольку проверка касается форм, а не поведения, она достаточно дешева для каждого запроса на слияние — а там расхождение находится до слияния. Однако там, где удаление намеренно, версия поднимается, а старый контракт остаётся соблюдённым, пока не закроется окно устаревания.