$ sudo teach|
    $ sudo teach|
    IT school
  • Telegram
  • Партнёрам
  • Все курсы
$ sudo teach IT
OOO "SALEPROFIT"Контакты и реквизитыIT-Park Logo

Школа

  • Блог
  • Проверить сертификат

Сотрудничество

  • Стать учителем
  • Партнёрская программа
  • О проекте

Право

  • Оферта
  • Политика конфиденциальности

© 2023–2026 $ sudo teach IT™. All Rights Reserved. Public user contributions licensed under CC BY-SA 4.0 license with attribution required
TelegramGitHubYouTube
ГлавнаяБлогПишем тесты для API как договор: матрица сценариев и проверка контрактов

Пишем тесты для API как договор: матрица сценариев и проверка контрактов

$ sudo teach IT
·21 июля 2026 г.·10 мин·53
Пишем тесты для API как договор: матрица сценариев и проверка контрактов

Соберём минимально достаточный набор тестов: валидация схем, статусы, поля ошибок и регрессии на контракте без хрупких моков.

Содержание
Что значит тестировать API как договорМинимальный набор тестов: что реально нужноМатрица сценариев: как не утонуть в комбинацияхКак строить матрицуПринцип: тесты проверяют форму и правила, а не детали реализацииПочему хрупкие моки — злоКонтракт ответов: единый формат ошибок и проверяемостьРекомендованный минимальный error contractПример эндпоинта и тест-матрицыМатрица для POST /usersКак проверять схемы: тесты против изменений JSONПример на TypeScript + Jest + AJVПроверка статусов и контрактов ошибок без привязки к текстамПрактика: тестируем error.code, а не messageРегрессия контракта: как тестировать «не исчезни»Регрессия: отдельный слой проверкиИзбегаем хрупкости: что не делать в тестахСтратегия для нескольких эндпоинтов: повторяемость подходаКак встроить тесты в процесс релизовТипичные ошибки при тестировании контрактов1) Слишком строгая схема и “вечные падения”2) Разный формат ошибок в разных частях приложения3) “Тестируем только успех”

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

В этой статье разберём подход, который ближе к идее «контракта»: тесты проверяют договор между клиентом и сервером — структуру данных, статусы, семантику ошибок и регрессии на стабильность интерфейса. Наша цель — минимально достаточный набор тестов: не «покрыть всё», а закрепить ключевые свойства, по которым API можно безопасно развивать.


Что значит тестировать API как договор

Договор API состоит из нескольких слоёв:

  1. Схема (schema) — типы, обязательность полей, форматы (email, UUID, даты), ограничения (минимум/максимум, regex), допустимые значения enum.
  2. Протокол (protocol) — HTTP-статусы, методы, редиректы, заголовки, а также соглашения об идемпотентности и пагинации (если применимо).
  3. Семантика ошибок (error contract) — структура тела ошибки, коды/типы ошибок, поля message, code, details, правила, когда возвращается 4xx и когда 5xx.
  4. Поведенческие инварианты (invariants) — условия истинности: «созданный ресурс доступен по GET», «обновление меняет только заявленные поля», «валидация не принимает некорректные данные».
  5. Регрессии контракта — чтобы изменения в бэкенде не ломали клиентов.

Когда тесты ориентированы на договор, они становятся устойчивее к внутренним рефакторингам. Мы тестируем не то, как устроен код, а то, что обязано быть стабильным.


Минимальный набор тестов: что реально нужно

Минимальный «адекватный» набор обычно включает:

  • Позитивные сценарии: валидные входные данные → ожидаемый статус → ожидаемая форма ответа → ключевые инварианты.
  • Негативные сценарии валидации: неверные входные данные → 400/422 (как у вас принято) → корректная структура ошибки → корректное содержание полей ошибок.
  • Проверки статусов: ресурс не найден → 404, нет прав → 403, конфликт → 409, серверная ошибка → 500 (или ваша специфика).
  • Проверки контракта ошибок: единый формат тела ошибки независимо от типа проблемы.
  • Регрессии схемы: тесты, которые ловят изменения в JSON-формате (например, удаление поля, смена типа с string на number, переименование ключа).
  • Идемпотентность/повторяемость (опционально): если эндпоинт таков, добавляем тесты на повторные запросы и воспроизводимость ответа.

Что не обязательно в минимуме:

  • Точные тексты сообщений, если они локализуются или могут меняться.
  • Мокирование внутренних компонентов, если можно сделать интеграционные тесты на тестовой среде (хотя бы минимальные).
  • Слишком детальные проверки «каждого поля» в каждом тесте — лучше проверять ключевые части схемы и инварианты.

Матрица сценариев: как не утонуть в комбинациях

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

Как строить матрицу

Для каждого эндпоинта определите:

  1. Варианты входа:
    • валидный (happy path)
    • невалидный — неправильный тип
    • невалидный — нарушение формата (например, email/UUID)
    • невалидный — пусто/пропущено обязательное
    • невалидный — бизнес-ограничения (например, «password слишком короткий»)
  2. Варианты ресурса (если есть):
    • ресурс существует
    • ресурс не существует
    • ресурс в состоянии, не позволяющем операцию (например, статус archived)
  3. Варианты прав:
    • аутентифицирован, но нет прав
    • есть права
  4. Варианты инфраструктуры:
    • конкурентные изменения (опционально)
    • внутренние ошибки (обычно один тест на «не упали, отдали 500 и контракт ошибки соблюдён»)

Матрицу можно формализовать, но лучше начать с компактного перечня «канонических» случаев. Практика показывает: если стабилизировать контракт ошибок и схемы, 70% ценности достигается.


Принцип: тесты проверяют форму и правила, а не детали реализации

Почему хрупкие моки — зло

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

В идеале вы делаете тесты на уровне API:

  • поднятый приложение (иногда с тестовой БД),
  • реальные HTTP-запросы,
  • проверка ответа по контракту.

Если мокирование неизбежно (например, внешние платежи), мокайте внешние зависимости, но контракт API не мокаем. Клиентская часть должна видеть тот же формат ошибок и статусы, что и в продакшене.


Контракт ответов: единый формат ошибок и проверяемость

Большая часть «хрупкости» появляется из-за того, что разные ветки возвращают разные структуры ошибок. Даже если статус один и тот же, клиенты могут зависеть от полей тела ошибки.

Рекомендованный минимальный error contract

Например, договором может быть:

  • HTTP-статус: 400/422 для валидации, 404 для отсутствия ресурса, 409 для конфликтов.
  • Тело ошибки: объект со стабильными полями, которые присутствуют всегда.

Пример (условный):

code
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request is invalid",
    "details": [
      {
        "field": "email",
        "reason": "must be a valid email"
      }
    ]
  }
}

В тестах важны:

  • наличие error.code,
  • наличие error.details (если вы их используете),
  • соответствие состава details ожидаемым полям,
  • отсутствие «лишней» структуры, которую вы потом будете поддерживать.

Не обязательно жестко сравнивать message, если он может меняться. Но важно, чтобы code и формат details были стабильными.


Пример эндпоинта и тест-матрицы

Допустим, у нас API для создания пользователя.

  • POST /users
  • успешный ответ: 201 Created
  • тело пользователя: id, email, name
  • ошибки валидации: 422 Unprocessable Entity
  • ошибки: единый формат

Матрица для POST /users

Минимально достаточный набор:

  1. Успех

    • email валиден
    • name валиден
    • → 201, схема ответа соблюдена, id появляется
  2. Валидация: отсутствует email

    • → 422, error.code = VALIDATION_ERROR
    • details содержит field = "email"
  3. Валидация: email невалидный

    • → 422, details содержит email
  4. Валидация: name пустой/слишком короткий

    • → 422, details содержит name
  5. Ошибка контракта на 500

    • Спровоцировать внутреннюю ошибку (например, принудительно выбросить исключение через специальный флаг в тестовой среде или дефектный сценарий)
    • → 500, ошибка возвращается в том же формате (или в вашем «серверном» контракте)
  6. Регрессия схемы

    • Даже если «сценарий успеха» уже есть, отдельный тест (или часть) проверяет, что ответ не потерял обязательные поля и типы

Как проверять схемы: тесты против изменений JSON

Есть два подхода:

  1. JSON Schema проверка (валидируем структуру ответа целиком).
  2. Проверка выборочных полей (дешевле, но слабее защищает от частичных изменений).

Для минимального набора чаще достаточно комбинации:

  • валидируем объект по схеме (или хотя бы ключевые поля),
  • отдельно проверяем поля ошибки и их содержание.

Пример на TypeScript + Jest + AJV

Допустим, вы используете TypeScript и хотите проверять JSON-ответы по схеме.

Установим ajv:

code
npm i ajv

Схема успеха (условно, упрощённо):

code
// schemas/userCreateResponse.ts
export const userCreateResponseSchema = {
  type: "object",
  required: ["id", "email", "name"],
  properties: {
    id: { type: "string", minLength: 1 },
    email: { type: "string" },
    name: { type: "string" }
  },
  additionalProperties: false
};

Схема ошибки:

code
// schemas/errorResponse.ts
export const errorResponseSchema = {
  type: "object",
  required: ["error"],
  properties: {
    error: {
      type: "object",
      required: ["code", "message", "details"],
      properties: {
        code: { type: "string" },
        message: { type: "string" },
        details: {
          type: "array",
          items: {
            type: "object",
            required: ["field", "reason"],
            properties: {
              field: { type: "string" },
              reason: { type: "string" }
            },
            additionalProperties: false
          }
        }
      },
      additionalProperties: false
    }
  },
  additionalProperties: false
};

Тест на проверку схемы:

code
// tests/users.create.contract.test.ts
import Ajv from "ajv";
import { userCreateResponseSchema } from "../schemas/userCreateResponse";
import { errorResponseSchema } from "../schemas/errorResponse";

const ajv = new Ajv({ allErrors: true, strict: false });

describe("POST /users contract", () => {
  it("success: returns user with expected schema", async () => {
    const res = await fetch("http://localhost:3000/users", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ email: "ava@example.com", name: "Ava" })
    });

    expect(res.status).toBe(201);

    const json = await res.json();

    const validate = ajv.compile(userCreateResponseSchema);
    const ok = validate(json);

    expect(ok).toBe(true);
    if (!ok) {
      // полезно, чтобы в лог шли конкретные проблемы схемы
      // в реальном проекте — выводить validate.errors
      console.error(validate.errors);
    }

    // Доп. инварианты (точечно, не дублируя schema):
    expect(json.email).toBe("ava@example.com");
    expect(typeof json.id).toBe("string");
  });

  it("validation error: missing email returns error contract", async () => {
    const res = await fetch("http://localhost:3000/users", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ name: "Ava" })
    });

    expect(res.status).toBe(422);

    const json = await res.json();

    const validate = ajv.compile(errorResponseSchema);
    const ok = validate(json);
    expect(ok).toBe(true);

    // Проверка семантики: ожидаем поле email в details
    const details = json.error.details as Array<{ field: string; reason: string }>;
    expect(details.some(d => d.field === "email")).toBe(true);
  });
});

Важные нюансы:

  • additionalProperties: false помогает ловить «утечки» или неожиданные поля. Иногда это слишком строго; можно ослабить и проверять только required.
  • Для больших схем держите их в отдельном месте (или генерируйте из OpenAPI).
  • Ошибки схемы — это именно то, что нужно, чтобы тесты не были хрупкими к локализации текста message.

Проверка статусов и контрактов ошибок без привязки к текстам

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

Практика: тестируем error.code, а не message

Текст ошибки часто меняется из-за правок UX или локализации. Поэтому:

  • error.code и структура details должны быть проверяемы,
  • message можно валидировать по схеме, но не сравнивать дословно.

Пример проверки «код ошибки и поле»:

code
expect(json.error.code).toBe("VALIDATION_ERROR");
expect(json.error.details.map((d: any) => d.field)).toContain("email");

Регрессия контракта: как тестировать «не исчезни»

Есть тип ошибок, которые сценарии ловят плохо: сервер начинает возвращать другое имя поля, отменяет обязательность или меняет формат id. В такой ситуации тест «happy path» может не упасть, если вы проверяете только часть полей.

Регрессия: отдельный слой проверки

Минимально достаточный способ:

  • На успехе проверяйте схему ответа (или хотя бы список обязательных полей и их типы).
  • На ошибках проверяйте схему ошибки (и что error.code/details присутствуют).

Если вы используете OpenAPI, ещё лучше:

  • хранить schema как часть спецификации,
  • генерировать валидатор,
  • и тестировать, что фактический ответ соответствует контракту.

Избегаем хрупкости: что не делать в тестах

  1. Не мокайте внутренние компоненты так, чтобы проверять их взаимодействия.
    Тест должен подтверждать внешний эффект и формат.

  2. Не сравнивайте порядок элементов, если семантика не требует порядка.
    Для details сортируйте или проверяйте множеством (в JS это удобно через some/includes по полям).

  3. Не привязывайтесь к точному тексту message.
    Проверяйте коды, поля и типы.

  4. Не делайте один огромный «энд-ту-энд» тест на всё.
    Лучше матрицей по ключевым веткам: валидация, доступ, не найдено, успех.

  5. Не позволяйте тестам «знать слишком много».
    Если внутренний формат не важен — не проверяйте его. Проверяйте то, что клиенту нужно.


Стратегия для нескольких эндпоинтов: повторяемость подхода

Контракты лучше проверять единообразно. Для этого удобно ввести вспомогательные функции:

  • assertStatus(res, expected)
  • assertJsonSchema(json, schema)
  • assertErrorCode(json, code)
  • assertValidationField(json, field)

Так вы снижаете стоимость поддержки тестов и минимизируете расхождения между командами.

Пример вспомогательных функций (условно, TypeScript):

code
import Ajv from "ajv";

const ajv = new Ajv({ allErrors: true, strict: false });

export function assertSchema(schema: any, json: any) {
  const validate = ajv.compile(schema);
  const ok = validate(json);
  if (!ok) throw new Error(`Schema validation failed: ${JSON.stringify(validate.errors)}`);
}

export function assertErrorField(json: any, field: string) {
  const details = json?.error?.details;
  if (!Array.isArray(details)) throw new Error("Expected error.details to be an array");
  if (!details.some((d: any) => d.field === field)) {
    throw new Error(`Expected error.details to contain field=${field}`);
  }
}

Как встроить тесты в процесс релизов

Тесты контрактов должны быть быстрыми и воспроизводимыми:

  • Запускать в CI после сборки,
  • Держать минимальный набор эндпоинтов «критичным»,
  • Разделять:
    • contract tests (быстро, схематично),
    • integration tests (глубже, дольше),
    • e2e (по необходимости).

Если тесты контрактов начинают длиться долго — чаще всего проблема в инфраструктуре (БД, миграции, поднятие окружения). Оптимизация должна идти туда: быстрее поднимать окружение, использовать тестовую БД с кэшированием, фиксировать начальные данные.


Типичные ошибки при тестировании контрактов

1) Слишком строгая схема и “вечные падения”

Если вы запретили additionalProperties: false, а сервер иногда добавляет метаданные — тесты начнут ломаться при каждом расширении ответа. В таком случае лучше:

  • разрешить дополнительные поля,
  • требовать только required,
  • или сделать schema уровня «минимум» для контрактного слоя.

2) Разный формат ошибок в разных частях приложения

Одна из самых дорогих проблем — когда валидация летит через один middleware, а бизнес-ошибки формируются другим способом. Контракт ломается, тесты выстраиваются «по месту», и поддержка превращается в работу следователя. Нужен единый error formatter, а тест должен закреплять именно его.

3) “Тестируем только успех”

Если вы проверяете только happy path, вы ловите проблемы схемы, но не ловите деградацию валидации и ошибок — а клиентам именно ошибки важны. Матрица сценариев должна включать минимум

Войдите, чтобы поставить лайк и оставить комментарий.

Автор

$ sudo teach IT

Продолжите обучение

Все курсы
Python – для начинающих!

Python – для начинающих!

С нуля до профессионального уровня. Подходит для всех. Учитесь каждый день и овладейте самым популярным языком программирования.

Перейти к курсу
Простыми словами о защищенном соединении

Простыми словами о защищенном соединении

Базовый курс для обычных людей: что такое защищённое соединение, как оно устроено, что оно умеет и чего не умеет, как выбрать сервис и настроить его на своих устройствах.

Перейти к курсу
Ботостроение Telegram

Ботостроение Telegram

Лёгкий, быстрый и доступный способ познакомиться с миром ботостроения в Telegram. Видео, конспекты, практика и помощь – всё у нас на курсе.

Перейти к курсу
Сервис, который не падает - как построить безопасную зашифрованную сеть

Сервис, который не падает - как построить безопасную зашифрованную сеть

Практический курс по безопасности сети: как собрать собственный сервис защищённого доступа - от пустого сервера до подписки с автовыбором - и, что важнее, как потом не уронить его своими руками.

Перейти к курсу

Другие статьи

Пишем CLI для разработчика на Python: команды, конфиги и корректные exit codes по стандарту проекта
пишем

Пишем CLI для разработчика на Python: команды, конфиги и корректные exit codes по стандарту проекта

Построим удобный интерфейс командной утилиты: структура подкоманд, чтение конфигов, валидация входных параметров и единая логика ошибок с понятными exit code. Сфокусируемся на UX для тех, кто запускает CLI в CI и в скриптах.

15 сентября 2026 г.
1030
Пишем корректные bash-скрипты: обработка ошибок, строгие режимы и безопасные переменные
пишем

Пишем корректные bash-скрипты: обработка ошибок, строгие режимы и безопасные переменные

Покажем best practices: set -euo pipefail, ловля ошибок, корректное quoting и проверка входных параметров. Разберём мини-проект с устойчивым поведением.

24 июля 2026 г.
720
Создание Telegram бота в 2026 легко и просто! Полный курсы!
создание

Создание Telegram бота в 2026 легко и просто! Полный курсы!

19 июня 2026 г.
1431
Как выбрать, куда идти в IT: веб, боты, данные — без хаоса
выбрать

Как выбрать, куда идти в IT: веб, боты, данные — без хаоса

Разберём самые понятные направления для новичка (веб, боты, аналитика/данные) и чем они отличаются по шагам обучения. В конце вы получите простой план, как проверить, что вам подходит, и какой стартовый путь выбрать.

5 октября 2026 г.
580
Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть
путь

Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть

Разберём структуру репозитория, как упаковать проект в понятную историю (проблема → решение → запуск → метрики качества) и какие артефакты повышают доверие: скринкасты, архитектурная схема, чек-лист продакшн-готовности.

17 сентября 2026 г.
1090
Как понять, какой язык программирования учить первым: простой тест и выбор
понять

Как понять, какой язык программирования учить первым: простой тест и выбор

Помогу новичку выбрать первый язык без стресса: объясню варианты по целям (веб, боты, анализ данных), что учить в первую очередь и как не потратить месяцы впустую. В конце — краткий план старта на ближайшие недели.

2 октября 2026 г.
630

Комментарии

Станьте первым в обсуждении

Поделитесь мнением о статье или задайте вопрос автору

Содержание

Что значит тестировать API как договорМинимальный набор тестов: что реально нужноМатрица сценариев: как не утонуть в комбинацияхКак строить матрицуПринцип: тесты проверяют форму и правила, а не детали реализацииПочему хрупкие моки — злоКонтракт ответов: единый формат ошибок и проверяемостьРекомендованный минимальный error contractПример эндпоинта и тест-матрицыМатрица для POST /usersКак проверять схемы: тесты против изменений JSONПример на TypeScript + Jest + AJVПроверка статусов и контрактов ошибок без привязки к текстамПрактика: тестируем error.code, а не messageРегрессия контракта: как тестировать «не исчезни»Регрессия: отдельный слой проверкиИзбегаем хрупкости: что не делать в тестахСтратегия для нескольких эндпоинтов: повторяемость подходаКак встроить тесты в процесс релизовТипичные ошибки при тестировании контрактов1) Слишком строгая схема и “вечные падения”2) Разный формат ошибок в разных частях приложения3) “Тестируем только успех”