← Назад к новостям
Автотесты: что есть 100% покрытие API? Часть 2
📰 Habr
📅 06.07.2026 09:11
⏱️ 12 мин
Почему GET STATIC OBJECT не нуждается в RequestPermissions, а GET DYNAMIC — обязан? Ку! В первой части Автотесты: что есть 100% покрытие API? Часть 1 я разложил методы на 11 подтипов и дал таблицу — какой тест-кейс к какому подтипу применим. Таблица получилась полезная, но плоская: "применимо / приоритет 1/2/3" — и всё. А самый частый вопрос, который мне прилетел в комментариях и в личку, был не "а где взять такую таблицу", а "а почему именно так? Почему у GET LIST OBJECTS нет RequestIncorrectBody, а у POST CREATE OBJECT есть?" Автотесты: что есть 100% покрытие API? Часть 1 Автотесты: что есть 100% покрытие API? Часть 2 Справедливый вопрос. Таблица без обоснования — это ещё один эксельник, только красивее оформленный. Поэтому в этой главе пройдёмся по всем 11 подтипам методов не построчно, а по существу: что это за объект, чем он рискует, какие тест-кейсы ему обязательны и почему, какие ему не нужны и почему, и что реально ломается, если тест-кейс пропустить. GET-семейство: один глагол, три разных риска Все три подтипа отвечают на GET, но риски у них принципиально разные, поэтому и наборы тест-кейсов не совпадают. GET STATIC OBJECT Это объект, данные которого не должны меняться никогда — справочники, конфиги, статичный контент. Ключевое слово — "не должны". Риск здесь один: кто-то в бэкенде решит, что "немного поменять" не страшно, и тихо сломает контракт. Обязательные тест-кейсы: RequestDefault и RequestCompareBenchmark . RequestDefault подтверждает, что эндпоинт вообще жив и отдаёт валидный по схеме ответ. RequestCompareBenchmark — это основной тест-кейс для этого подтипа, потому что для статичного объекта мы точно знаем эталонное значение и обязаны сверяться с ним построчно, а не "в рамках допустимого". А вот RequestPermissions сюда, как правило, не нужен — по определению статичный объект не содержит ничьих личных данных и не имеет ролевой модели доступа (если она вдруг появилась — поздравляю, это уже не GET STATIC OBJECT, а GET DYNAMIC OBJECT, потому что данные стали динамическими относительно роли). RequestsParams тоже не нужен — нечего фильтровать и сортировать в единственном статичном объекте. Живой пример, зачем это разделение вообще нужно: список поддерживаемых валют выглядит как GET STATIC OBJECT, но если завтра появляется региональное ограничение ("для пользователей из региона X валюта Y скрыта") — это уже не статика, и молчаливое "он же не менялся полгода, зачем на него тесты" превращается в баг, который полгода никто не ловил, потому что тест на этот эндпоинт вообще не считался нужным. GET DYNAMIC OBJECT Объект, который можно создать, отредактировать, удалить — карточка пользователя, пост, заказ. Риск сразу удваивается: во-первых, данные меняются, и мы обязаны видеть актуальное состояние; во-вторых, у динамического объекта почти всегда есть владелец, а значит — есть чужие данные, к которым нельзя получить доступ. Именно поэтому здесь RequestPermissions обязателен, а не опционален — это тест-кейс первого приоритета. Причина ровно в риске: динамический объект = потенциально личные данные. Плюс обязательны RequestNewObject и RequestUpdateObject — они проверяют не эндпоинт вообще, а конкретный жизненный цикл: что созданный объект стал видим именно в том виде, в каком его создали, а обновлённый — именно в обновлённом. RequestNotFound тоже жизненно важен именно тут (хотя формально применим и к другим подтипам) — динамический объект можно удалить, и запрос на удалённый/несуществующий id — это не экзотика, а рабочий сценарий каждый день. GET LIST OBJECTS Множество объектов с фильтрацией, сортировкой, пагинацией. Здесь центр тяжести смещается с "правильный ли объект" на "правильный ли набор объектов при заданных условиях" — отсюда RequestsParams как обязательный тест-кейс второго приоритета с четырьмя под-кейсами (фильтры, сортировки, пагинация, их комбинации). Казалось бы, RequestPermissions тут менее очевиден, чем для GET DYNAMIC OBJECT — но на самом деле рискованнее: список — это то место, где чаще всего утекают чужие объекты. Баг "фильтр по своим объектам не применился, и в списке видно чужие черновики" — это баг именно списка, а не единичного объекта, потому что единичный GET по чужому id вы бы и не подумали дёргать, а список отдаёт их сам, без явного запроса. Поэтому RequestPermissions для списков даже важнее, чем кажется на первый взгляд. POST-семейство: четыре подтипа, четыре разных смысла "создать" POST — это не один смысл, а четыре, и путать их — главная ошибка при составлении плана покрытия. POST CREATE OBJECT Полноценное создание сущности, часто с зависимостями (id черновика, id родителя). Здесь работает почти весь набор: RequestDefault , RequestCompareBenchmark , RequestPermissions , и обязательно RequestIncorrectBody — потому что создание принимает тело, а тело всегда можно сломать. К нему же — RequestElements с под-кейсами на обязательные/необязательные поля, границы, типы данных: это тест-кейс, который проверяет не "работает ли метод", а "что произойдёт, если разработчик закодил валидацию не так, как думал". Важный нюанс подтипа — зависимости. Если создание объекта требует id черновика из другого сервиса, то RequestElements обязан включать кейс с несуществующим/чужим id зависимости, а не только с отсутствующими полями самого объекта. Это то место, где регламент из первой части остаётся верным по букве, но требует уточнения по смыслу: "обязательные элементы" — это не только поля тела, но и внешние зависимости, если они есть у конкретного эндпоинта. POST UPLOAD FILE Формально тоже POST, но смысл настолько другой, что общий набор тест-кейсов для этого подтипа неполон без явного расширения. Регламент из первой части упоминает его вскользь ("может иметь ограничения на вес и формат"), и вот тут стоит зафиксировать явно, что помимо стандартного RequestElements этому подтипу обязательно нужны: проверка превышения лимита размера файла; проверка запрещённого MIME-типа (в том числе — с подменённым расширением, когда файл выдаёт себя не за то, чем является); проверка обоих способов передачи, если API поддерживает и json (base64), и form-data — это фактически два разных контракта, и покрытие одного не гарантирует покрытие второго. Если этого не зафиксировать отдельно, велик риск, что POST UPLOAD FILE молча "унаследует" стандартный набор от POST CREATE OBJECT и тест на лимит веса файла просто никто не напишет, потому что в общей таблице для него нет отдельной строки. POST CREATE OBJECT TO OBJECT Лайк, комментарий, реакция — создание сущности, привязанной к другой сущности. Смысловое отличие от POST CREATE OBJECT в том, что здесь всегда есть вторая сторона риска: помимо валидности новой сущности, важно, что происходит с родительским объектом. Отсюда обязательный, но не всегда очевидный кейс: что будет с родительским объектом, если он удалён или недоступен в момент создания дочерней сущности (комментарий к удалённому посту, лайк на скрытый контент). RequestPermissions здесь имеет двойную природу — права нужно проверять и на дочерний объект (может ли пользователь вообще комментировать), и на родительский (виден ли пользователю сам пост, к которому он пытается оставить комментарий). Это тот случай, где одна строчка регламента "Permissions обязателен" на практике разворачивается в два независимых теста, а не в один. POST FILTER/SORT/OBJECTS ON LIST По сути тот же GET LIST OBJECTS, но с телом запроса вместо query-параметров — обычно потому, что фильтров слишком много для строки запроса. Раз это список — весь набор рассуждений про RequestPermissions из GET LIST OBJECTS применим один в один. Но добавляется специфика POST: поскольку параметры теперь в теле, сюда возвращается RequestIncorrectBody (некорректный JSON тела фильтра — это не то же самое, что некорректный query-параметр, и обрабатывается кодом иначе), которого не было бы у обычного GET-списка. PATCH/PUT-семейство: одна цель, разная зона поражения PATCH OBJECT Частичное обновление объекта целиком (в смысле — вызов один, а не по элементам). Риск здесь специфичный: частичное обновление обязано не задевать поля, которые не передавались в запросе. Поэтому к стандартному набору обязательно добавляется сравнение "до и после" не только по изменённым полям, но и по неизменённым — баг "PATCH одного поля случайно обнулил соседнее" ловится только так, и в исходной таблице первой части это неявно спрятано внутри RequestUpdateObject , но стоит проговорить отдельно, потому что для PUT (ниже) это же самое проверяется зеркально наоборот. PATCH ELEM ON OBJECT Обновление конкретного элемента (или элементов) объекта — например, отдельно статус, отдельно тег. Отличие от PATCH OBJECT некритичное по механике, но критичное по площади тестирования: если у объекта десять независимо обновляемых элементов, это не один эндпоинт с одним набором тест-кейсов, а фактически десять зон риска с одной точкой входа. RequestElements здесь обязан прогоняться на каждый обновляемый элемент отдельно, а не один раз "для метода вообще" — иначе покрытие 100% по формальному признаку (тест-кейс есть) на деле означает, что девять элементов из десяти не проверялись никогда. PUT OBJECT В отличие от PATCH, PUT подразумевает полную замену объекта. Отсюда обратный PATCH-у риск: если в запросе не передано необязательное (по факту, а не по документации) поле — должно ли оно обнулиться или остаться прежним? Это довольно частый источник расхождения ожиданий фронта и бэка, и RequestOnlyRequiredElem (под-кейс RequestElements ) для PUT имеет отдельный смысл: он проверяет не "работает ли метод с минимальным набором данных", а "что случится с остальными полями объекта, если их не передать". DELETE OBJECT Самый короткий подтип по механике и один из самых недооценённых по рискам. RequestPermissions тут обязателен избыточно строго — удаление необратимо (или условно обратимо), и ошибка в правах здесь не "увидел чужое", а "снёс чужое". RequestNotFound тоже обязателен нетривиально: нужно проверять не только "удалить несуществующий объект", но и "удалить уже удалённый объект повторно" — это разные ветки кода, и разработчики нередко закрывают только первую. К этому же подтипу естественно тянется проверка каскада: что произошло с зависимыми объектами (комментарии к удалённому посту, файлы в удалённой папке) — формально это выходит за рамки одного эндпоинта DELETE, но по смыслу это часть его покрытия, и в исходной таблице первой части такого кейса нет вообще — стоит считать это уточнением к регламенту, а не забытой деталью. Сводка: где регламент из части 1 стоило бы уточнить Собрал в одном месте те моменты, где простое "применимо / приоритет" из первой таблицы не отражает разницу между подтипами: Подтип Что уточняет часть 2 GET STATIC OBJECT RequestPermissions не нужен, пока объект не приобрёл зависимость от роли/региона — тогда это уже не этот подтип GET DYNAMIC OBJECT RequestPermissions — не формальность, а первый приоритет из-за владельца данных GET LIST OBJECTS RequestPermissions важнее, чем для одиночного объекта — списки чаще всего утечка POST CREATE OBJECT RequestElements обязан включать не только поля тела, но и внешние зависимости POST UPLOAD FILE Нужен явный, не наследуемый набор: лимит веса, MIME/расширение, оба формата передачи POST CREATE OBJECT TO OBJECT RequestPermissions разворачивается в два теста — права на дочерний и на родительский объект POST FILTER/SORT/OBJECTS ON LIST RequestIncorrectBody возвращается в набор, т.к. параметры теперь в теле, а не в query PATCH OBJECT Обязательна проверка "до/после" по неизменённым полям, не только по изменённым PATCH ELEM ON OBJECT RequestElements гоняется на каждый независимый элемент отдельно, не один раз "за метод" PUT OBJECT RequestOnlyRequiredElem проверяет обнуление остального объекта, а не минимальную работоспособность DELETE OBJECT RequestNotFound включает повторное удаление; отдельно нужна проверка каскада на зависимые объекты Ничего из этого не отменяет таблицу из первой части — она остаётся верным списком "что вообще может быть". Но если использовать её буквально, без этих уточнений, легко получить формальные "100% покрытие" по чек-листу, которое на деле дырявое именно в тех местах, где подтипы отличаются друг от друга сильнее, чем показывает плоская таблица.
#Api
#Бот
#Чат
📊 Источник:
Habr
|
Оригинал