Тестирование 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, остальные разбираются в тренажёре.
- Как в запросе передают 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, с валидным — доступ по правам. - Зачем валидировать ответ API по схеме (например, JSON Schema)?A)Автоматически проверить структуру и типы всех полейB)Чтобы измерить размер ответа в байтах и убедиться, что он не превышает лимит каналаC)Чтобы ускорить парсинг ответа на клиенте за счёт заранее известной его структурыD)Чтобы зашифровать поля ответа согласно описанной в схеме модели данных сервиса
показать ответ и разбор
+A)Автоматически проверить структуру и типы всех полей// разбор: Точечные ассерты проверяют отдельные значения (id == 42), но не заметят, что поле переименовали, сменили тип (число стало строкой), убрали или добавили лишнее — а это ломает клиентов. Валидация по схеме (JSON Schema, Pydantic, OpenAPI) сверяет весь ответ с описанием контракта: набор полей, их типы, обязательность, форматы. Одной проверкой она ловит структурные регрессии на всём объекте, а не там, где тестировщик догадался поставить ассерт. Это делает контракт исполняемым: изменил ответ несовместимо — тест сразу красный. Особенно ценно при версионировании и на границе команд.
- Зачем в тесте подменять внешний сервис моком или стабом?A)Чтобы убрать сетевой код из функции на время прогона набора тестовB)Задать заранее известный ответ вместо реального сервисаC)Чтобы прогнать тест на боевом внешнем сервисе и получить самые реалистичные данныеD)Чтобы ускорить сам внешний сервис, кэшируя его ответы между разными тестами
показать ответ и разбор
+B)Задать заранее известный ответ вместо реального сервиса// разбор: Реальный вызов внешнего API делает тест зависимым от чужой системы: она может быть недоступна, медленна, отдавать меняющиеся данные или стоить денег/квоты за вызов — тест становится флаки и хрупким. Мок/стаб подменяет внешний сервис заданным ответом (нужный успех или ошибка), так что тест проверяет логику своего кода детерминированно и быстро, не выходя в сеть. Отдельно моки позволяют смоделировать труднодостижимые случаи: таймаут, 500, кривой ответ. Реальную интеграцию проверяют отдельными, более редкими интеграционными/e2e-тестами.
- Почему API-тесты держат в CI, а не гоняют вручную?A)Потому что вручную API-тесты технически не получится выполнить без специального сервераB)Потому что в CI тесты проверяют скорость ответа, а вручную — его корректностьC)Автопрогон на каждый коммит ловит регресс контракта до продаD)Потому что CI шифрует запросы, а при ручном прогоне данные идут открытым текстом
показать ответ и разбор
+C)Автопрогон на каждый коммит ловит регресс контракта до прода// разбор: API — контракт между командами и клиентами, и он ломается тихо: кто-то переименовал поле, сменил код ответа, добавил обязательный параметр — и интеграции падают. Если проверять это руками, проверка случается редко и нерегулярно, а между релизами регресс копится незамеченным. В CI набор API-тестов гоняется на каждый коммит/пулл-реквест автоматически: сломал контракт — сборка красная сразу, до слияния и до прода. Так дефект ловится там, где он дёшев, а не у потребителя по кривым данным. Ручные проверки остаются для разведочных и разовых сценариев.
- Зачем в Postman/автотестах API выносят значения в переменные окружения?A)Чтобы каждый запрос выполнялся быстрее за счёт кэширования значений переменныхB)Чтобы скрыть тело запроса от других членов команды, работающих с той же коллекциейC)Чтобы Postman сам генерировал тестовые данные вместо тестировщика при прогонеD)Один набор тестов гоняют против dev/stage/prod, меняя лишь baseUrl и токен, без правки самих запросов
показать ответ и разбор
+D)Один набор тестов гоняют против dev/stage/prod, меняя лишь baseUrl и токен, без правки самих запросов// разбор: Адрес API, токены, идентификаторы различаются между средами (dev, stage, prod). Если вписать их прямо в каждый запрос, для смены среды пришлось бы править все тесты — долго и с ошибками. Переменные окружения выносят эти значения в отдельный набор: запрос ссылается на {{baseUrl}}/users, а конкретный baseUrl подставляется из выбранного окружения. Так один и тот же набор тестов запускают против любой среды переключением окружения, а секреты не хардкодятся в тела запросов. Это делает тесты переносимыми и безопаснее в части хранения токенов.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.