сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Тестирование API

Тестирование API на практике

Практика проверки эндпоинта

Три случая из моих замеров, где код ответа зелёный, а всё плохо. Первый: 200 и тело {"error": "недостаточно средств"} - проверка «код 2xx значит успех» считает это успехом. Второй: 201 «создано», а записей в базе ноль или две. Третий: сумма приехала не числом, а строкой "1234.56" - код тот же, клиент сломается на сложении.

Стержень: проверяют статус, тело против схемы, заголовки и состояние системы после запроса, а негативные случаи обязательны.

// Формулировки: «как тестируешь эндпоинт?», «что проверяешь кроме кода ответа?», «какие негативные случаи гоняешь?».

Чек-лист эндпоинта

Эндпоинт - это отдельный адрес API, который делает одну вещь: отдаёт заказ, создаёт заказ, отменяет его. Проверяют его по порядку, который не даёт забыть половину. Первое - счастливый путь: валидный запрос, правильный код, правильное тело. Второе - проверка каждого поля: типы, обязательность, границы, длины. Третье - доступ: без токена, с чужой ролью, и обязательно со своей ролью, но к чужому объекту. Четвёртое - ошибки: чего нет (404), что конфликтует с состоянием (409), что не прошло проверку (422). Пятое - повторы: тот же запрос дважды, с ключом идемпотентности (уникальной меткой, по которой сервер узнаёт повтор) и без него. Шестое - сверка ответа с описанным договором.

Техники тест-дизайна ложатся сюда идеально, потому что вход у API строго структурирован. Классы эквивалентности - группы значений, которые система обрабатывает одинаково, так что из каждой хватит одного представителя: валидное, невалидное, пустое. Границы - на числа, длины строк, размеры страниц, потому что ошибаются обычно ровно на краю. Переходы состояний - на сущностях со статусом: из «нового» заказ может стать «оплаченным», а из «отменённого» уже нет.

// И правило про объём: проверить все сочетания параметров невозможно, поэтому берут попарные комбинации - такие наборы, где каждая пара значений встречается хотя бы раз, - плюс рискованные места. Это в разы меньше полного перебора при почти том же покрытии.

чек-лист эндпоинта
счастливый путь, поля, доступ, ошибки, повторы, договор
классы эквивалентности
группы значений, ведущие себя одинаково: хватит одного из группы

Проверять ответ целиком, а не код

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

Вторая: проверять заголовки. На создание должен приехать адрес созданного объекта, на ограничение частоты - подсказка, через сколько повторить, на кэшируемый ответ - метка версии. Заголовки - часть договора, а их обычно не смотрит никто.

// Третья, самая важная: проверять состояние системы после запроса. Ответ 201 сам по себе не значит, что запись появилась. Я иду в базу или дёргаю чтение и убеждаюсь, что объект ровно один. Классический дефект - повтор из-за перезапроса создал два заказа: ответы у обоих были прекрасные.

схема ответа
формальное описание полей и типов; тело сверяют с ним
проверка последствия
состояние в базе или событие после запроса, а не один лишь ответ

Негативные случаи и данные

Список, который стоит держать в голове для каждого принимающего поля. Пустое значение против отсутствия поля - это разные вещи, и сервер обязан различать: {"name": null} и {} часто обрабатываются по-разному, а тесты проверяют только второе. Лишние неизвестные поля - что сервер с ними сделает. Неверный тип содержимого в заголовке. Битое тело, которое не разбирается. Очень длинная строка и очень большое тело. Юникод, эмодзи, буквы других алфавитов и знаки-невидимки в строках.

Отдельно - границы чисел: ноль, отрицательное, максимум, максимум плюс один, дробное там, где ждут целое. И даты: конец месяца, високосный день, полночь, смена часового пояса.

// Данные под тесты готовят сами: создают через API или заранее заготовленным набором, чистят после себя. Тесты, живущие на «том самом пользователе на общем стенде», ломаются от чужого прогона, и разбираться в этом придётся вам, а не тому, кто его поменял.

null против отсутствия
поле со значением null и поле, которого нет, - разные случаи
свои данные на тест
создаются и убираются самим тестом, а не берутся с общего стенда

Как отвечать: «Как ты тестируешь эндпоинт?»

Иду по чек-листу, а не по наитию. Сначала счастливый путь: валидный запрос, правильный код, правильное тело. Дальше каждое поле - классы эквивалентности и границы, а следом негативные случаи: null против отсутствия поля, лишние неизвестные поля, битое тело, неверный тип содержимого, юникод и очень длинные строки. Потом доступ: без токена, с чужой ролью и обязательно со своей ролью, но к чужому объекту - именно там обычно и дыра. Потом ошибки: чего нет, что конфликтует с состоянием. Потом повторы: тот же запрос дважды, с ключом идемпотентности и без, и смотрю, не появился ли дубль. Ключевое - я проверяю ответ целиком, а не код. Сверяю тело со схемой, потому что видел, как сумма приезжает строкой вместо числа при том же коде. Смотрю заголовки, потому что они часть договора. И обязательно проверяю состояние после запроса: ответ двести один ещё не значит, что запись появилась, - бывает, что её нет или их две. Все данные готовлю сам и убираю за собой, чтобы тест не зависел от чужих прогонов.

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

На чём валят

  • Проверять только код ответа: 200 с ошибкой в теле и 201 без записи проходят насквозь.
  • Не сверять тело со схемой - смена типа поля доедет до живых пользователей и сломает клиента.
  • Пропустить разницу между null и отсутствующим полем: «обязательное» поле молча принимает null.
  • Жить на общих данных стенда - чужой прогон красит ваши тесты.
  • Забыть проверить последствие: ответ хороший, а записей в базе ноль или две.

Проверьте себя

Пять вопросов из банка по этой подтеме. Всего их 13, остальные разбираются в тренажёре.

  1. #api_test_practice1 / 5
    Как в запросе передают Bearer-токен и зачем?
    A)Токен передают в query-параметре URL, чтобы он попадал в логи и был виден в адресе
    B)Bearer-токен вписывают в тело каждого запроса рядом с основными данными полезной нагрузки
    C)Токен сохраняют в cookie, и браузер сам решает, к каким запросам его прикладывать
    D)В заголовке Authorization: Bearer <токен>; по нему сервер опознаёт и авторизует запрос
    показать ответ и разбор
    +D)В заголовке Authorization: Bearer <токен>; по нему сервер опознаёт и авторизует запрос

    // разбор: Bearer-токен — распространённый способ авторизации API. Клиент получает токен при логине и в каждом защищённом запросе кладёт его в заголовок Authorization со схемой Bearer: Authorization: Bearer eyJ.... Сервер извлекает токен, проверяет подпись и срок, определяет пользователя и его права и решает, пропускать ли запрос. «Bearer» значит «предъявитель»: кто владеет токеном, тот и авторизован — поэтому его берегут (HTTPS, короткий срок). Тестировщик проверяет: без токена — 401, с истёкшим/подделанным — 401/403, с валидным — доступ по правам.

  2. #api_test_practice2 / 5
    Зачем валидировать ответ API по схеме (например, JSON Schema)?
    A)Автоматически проверить структуру и типы всех полей
    B)Чтобы измерить размер ответа в байтах и убедиться, что он не превышает лимит канала
    C)Чтобы ускорить парсинг ответа на клиенте за счёт заранее известной его структуры
    D)Чтобы зашифровать поля ответа согласно описанной в схеме модели данных сервиса
    показать ответ и разбор
    +A)Автоматически проверить структуру и типы всех полей

    // разбор: Точечные ассерты проверяют отдельные значения (id == 42), но не заметят, что поле переименовали, сменили тип (число стало строкой), убрали или добавили лишнее — а это ломает клиентов. Валидация по схеме (JSON Schema, Pydantic, OpenAPI) сверяет весь ответ с описанием контракта: набор полей, их типы, обязательность, форматы. Одной проверкой она ловит структурные регрессии на всём объекте, а не там, где тестировщик догадался поставить ассерт. Это делает контракт исполняемым: изменил ответ несовместимо — тест сразу красный. Особенно ценно при версионировании и на границе команд.

  3. #api_test_practice3 / 5
    Зачем в тесте подменять внешний сервис моком или стабом?
    A)Чтобы убрать сетевой код из функции на время прогона набора тестов
    B)Задать заранее известный ответ вместо реального сервиса
    C)Чтобы прогнать тест на боевом внешнем сервисе и получить самые реалистичные данные
    D)Чтобы ускорить сам внешний сервис, кэшируя его ответы между разными тестами
    показать ответ и разбор
    +B)Задать заранее известный ответ вместо реального сервиса

    // разбор: Реальный вызов внешнего API делает тест зависимым от чужой системы: она может быть недоступна, медленна, отдавать меняющиеся данные или стоить денег/квоты за вызов — тест становится флаки и хрупким. Мок/стаб подменяет внешний сервис заданным ответом (нужный успех или ошибка), так что тест проверяет логику своего кода детерминированно и быстро, не выходя в сеть. Отдельно моки позволяют смоделировать труднодостижимые случаи: таймаут, 500, кривой ответ. Реальную интеграцию проверяют отдельными, более редкими интеграционными/e2e-тестами.

  4. #api_test_practice4 / 5
    Почему API-тесты держат в CI, а не гоняют вручную?
    A)Потому что вручную API-тесты технически не получится выполнить без специального сервера
    B)Потому что в CI тесты проверяют скорость ответа, а вручную — его корректность
    C)Автопрогон на каждый коммит ловит регресс контракта до прода
    D)Потому что CI шифрует запросы, а при ручном прогоне данные идут открытым текстом
    показать ответ и разбор
    +C)Автопрогон на каждый коммит ловит регресс контракта до прода

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

  5. #api_test_practice5 / 5
    Зачем в Postman/автотестах API выносят значения в переменные окружения?
    A)Чтобы каждый запрос выполнялся быстрее за счёт кэширования значений переменных
    B)Чтобы скрыть тело запроса от других членов команды, работающих с той же коллекцией
    C)Чтобы Postman сам генерировал тестовые данные вместо тестировщика при прогоне
    D)Один набор тестов гоняют против dev/stage/prod, меняя лишь baseUrl и токен, без правки самих запросов
    показать ответ и разбор
    +D)Один набор тестов гоняют против dev/stage/prod, меняя лишь baseUrl и токен, без правки самих запросов

    // разбор: Адрес API, токены, идентификаторы различаются между средами (dev, stage, prod). Если вписать их прямо в каждый запрос, для смены среды пришлось бы править все тесты — долго и с ошибками. Переменные окружения выносят эти значения в отдельный набор: запрос ссылается на {{baseUrl}}/users, а конкретный baseUrl подставляется из выбранного окружения. Так один и тот же набор тестов запускают против любой среды переключением окружения, а секреты не хардкодятся в тела запросов. Это делает тесты переносимыми и безопаснее в части хранения токенов.

дальше

Теорию прочитали. Навык ставится повторением

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