Почему GET STATIC OBJECT не нуждается в RequestPermissions, а GET DYNAMIC — обязан?
Ку! В первой части Автотесты: что есть 100% покрытие API? Часть 1 я разложил методы на 11 подтипов и дал таблицу — какой тест-кейс к какому подтипу применим. Таблица получилась полезная, но плоская: "применимо / приоритет 1/2/3" — и всё. А самый частый вопрос, который мне прилетел в комментариях и в личку, был не "а где взять такую таблицу", а "а почему именно так? Почему у GET LIST OBJECTS нет RequestIncorrectBody, а у POST CREATE OBJECT есть?"
Справедливый вопрос. Таблица без обоснования — это ещё один эксельник, только красивее оформленный. Поэтому в этой главе пройдёмся по всем 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% покрытие" по чек-листу, которое на деле дырявое именно в тех местах, где подтипы отличаются друг от друга сильнее, чем показывает плоская таблица.
