Есть класс ошибок, которые проходят все ручные проверки и спокойно доезжают до продакшена. Не пятисотки, не пустые поля, не сломанная структура — с этим как раз всё в порядке. Речь про случаи, когда сервис отвечает статусом 200, все поля на месте, типы правильные, и при этом одно число посчитано неверно.
Расскажу, как я проверяю такие ответы, когда в команде нет автотестов и в ближайшее время не появится: писать и поддерживать код некому, а сверка ответа с документацией делается глазами. Подход применим в любом инструменте, я просто покажу его на своём рабочем сценарии — создании заказа с расчётом скидки.
Что написано в документации
В нашей постановке блок с ценами описан так, как это обычно и выглядит в Confluence: таблица параметров, тип, обязательность и правило вычисления.
Поле | Тип | Обязательное | Правило |
|---|---|---|---|
| целое | да | Сумма позиций без скидки |
| целое | да | 10% от |
| целое | да |
|
| строка | да | Всегда |
| целое | да | Для этого тарифа всегда |
Ключевое здесь — третья строка. total не приходит откуда‑то извне, он обязан быть равен разнице двух других полей того же ответа. Именно такие правила чаще всего и ломаются: сумма считается в одном сервисе, скидка применяется в другом, а собирается ответ в третьем.
Что вернул сервис
Вот сокращённый ответ на создание заказа:
{ "traceId": "7c2a8410-56f2-4f59-b921-202607230002", "orderId": "ORD-2026-0723-001", "status": "CREATED", "customerId": "C-1042", "pricing": { "subtotal": 51960, "discount": 5196, "total": 47764, "currency": "RUB", "vatAmount": 0 } }
Теперь немного интерактивности, посмотрите на него секунд десять, как вы обычно смотрите на ответ в клиенте. Статус 200, структура совпадает с документацией, ни одного пустого или неожиданного значения. Скидка посчитана правильно: 10% от 51 960 — это 5196.
А total неверный. 51 960 минус 5196 — это 46764, а пришло 47764. Ровно тысяча рублей сверху.
Проблема не в том, что ошибку невозможно заметить. Проблема в том, что заметить её можно только одним способом — взять калькулятор и посчитать самому. На одном ответе это полминуты. На двадцати ответах в спринте, где ещё десяток полей и несколько сценариев расчёта, этого не делает никто. Люди смотрят, что поля на месте и значения «похожи на правду», и идут дальше.
Почему проверка «поле равно значению» тут не помогает
Первое, что приходит в голову, — зафиксировать ожидаемые значения. Взять ответ, сохранить его как эталон и сравнивать с ним следующие.
Так делать нельзя, и вот почему. Если вы возьмёте текущий ответ за эталон, вы зафиксируете total равным 47 764 — то есть закрепите ошибку как норму. Дальше проверка будет добросовестно подтверждать, что сервис стабильно считает неправильно. Когда разработчик починит расчёт, тест покраснеет — и вы пойдёте «чинить тест», приводя его обратно к сломанному поведению.
Это, кстати, общая ловушка любой генерации проверок по образцу ответа: и автогенератора в инструменте, и языковой модели, которой вы скормите JSON. Всё, что построено от факта, описывает факт, а не требование. Документация и ответ — два разных источника истины, и проверка обязана расти из первого.

total он предлагает сравнивать с 47 764 — то есть с текущим, неверным значением.Поэтому сгенерированный набор — это заготовка, а не результат. Дальше её нужно править по документации, и это как раз та работа, ради которой человек в процессе и нужен.
Как выглядит проверка, построенная от документации
Из пяти полей блока pricing три проверяются тривиально, а два требуют вычисления.
Простые — это currency и vatAmount. Документация говорит «всегда RUB» и «всегда 0», значит, сравнение с константой здесь корректно: мы сверяем не с фактом, а с требованием, которое случайно совпало с фактом.
Со скидкой чуть сложнее: она не константа, а функция от суммы. Значит, subtotal нужно сначала запомнить, а discount сравнить с округлённым результатом вычисления по этому запомненному значению. И то же самое с итогом: запоминаем discount, считаем ожидаемый total как разницу и сравниваем с фактическим.

Получается цепочка, которая проверяет не значения, а правило. Она останется валидной, когда изменится сумма заказа, когда поменяется состав позиций и когда придёт другой клиент. Ломаться она будет ровно в одном случае — если сервис посчитает неправильно. Это и есть то, чего мы добиваемся.
Лишние поля: почему перечислять нужные бесполезно
Второй тип ошибок, который ручная сверка пропускает почти всегда, — не отсутствие ожидаемого, а появление неожиданного. Человек проверяет по списку из документации: subtotal есть, discount есть, total есть, currency есть, vatAmount есть. Все пять на месте, проверка пройдена. А в ответе их шесть, и шестое поле никто не заметил, потому что его не было в списке, по которому сверяли.
Между тем лишнее поле — это довольно серьёзно. Оно может быть внутренним флагом, случайно вылезшим наружу, отладочным значением или, что хуже, данными, которых в этом ответе быть не должно.
Лечится это одной проверкой: перечисляем разрешённый состав ключей и требуем, чтобы других не было. Одна строка закрывает весь блок целиком и продолжает работать, когда в документацию добавят новое поле — тогда проверка честно покраснеет, и вы обновите список осознанно, а не задним числом.

Куда смотреть, когда проверка покраснела
Итак, прогон дал ожидаемый результат: четыре проверки зелёные, одна красная — фактический итог не сошёлся с вычисленным.

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

Принципиально здесь то, что это не «посмотреть логи глазами», а те же самые проверки, только по другому источнику данных. Условие «на шаге расчёта итог должен быть равен разнице» формулируется один раз и применяется и к ответу сервиса, и к записи в логе.
Что происходит на следующем релизе
Вся описанная работа заняла у меня минут пятнадцать: вставить ответ, получить заготовку, поправить четыре шага по документации, добавить проверку состава и связать с логами.
Но ценность не в первом запуске — пятнадцать минут можно и калькулятором отработать. Ценность в том, что на следующем релизе это уже не работа: открыл, нажал «запустить», получил тот же отчёт. И через месяц, когда в расчёт добавят НДС, вы поменяете одно правило вместо того, чтобы заново вспоминать, как вообще считается итог в этом сервисе.
Второй эффект — передаваемость. Проверка перестаёт жить в голове того, кто её придумал. Коллега открывает сохранённый сценарий и получает тот же результат, не задавая вопросов. Для команд, где тестирование размазано между аналитиком, разработчиком и второй линией поддержки, это часто важнее скорости.
Где этот подход не нужен
Скажу прямо, чтобы не тратить ваше время.
Если в команде уже есть автотесты, настроенный пайплайн и человек, который их поддерживает, — вам ничего из этого не нужно, задача давно решена лучше. Если проверки должны запускаться на каждый коммит, тоже мимо: описанное я делаю в настольном приложении под Windows, оно запускается руками, а не из CI. И если проверка нужна ровно один раз и повторять её никто не собирается, калькулятор быстрее.
Подход имеет смысл в другой ситуации, довольно частой: проверять API нужно регулярно, ошибки в расчётах болезненны, а кода писать некому — ни своих автотестов, ни инженера под них, ни, зачастую, разрешения отправлять реальные данные во внешние сервисы.
Если у вас есть свой способ ловить такие расхождения без написания кода — расскажите в комментариях, мне правда интересно, как это решают другие.

