Взлом
Уважаемые гости! При посещении нашего сайта просим вас ознакомиться с разделами форума, прежде чем оставлять ваши объявления и т.п., а также при обращении за помощью просим быть внимательными: на сайте есть как проверенные специалисты, так и непроверенные. Если вы обратились к специалисту, который проверку НЕ проходил, рекомендуем воспользоваться услугой гарант-сервиса. Спасибо, что посетили XakerPlus!

Ошибка в расчёте скидки, которую не видно глазами: проверяем ответ API по документации, когда автотестов нет

📌 Как поднять тему без нарушений

Не допускаются бессодержательные комментарии: «Ап»,«Up»,«Актуально»«Работаю» и прочие однотипные сообщения.

✅ Как поднять/обновить тему правильно: Напишите что изменилось в услугах, какие новости, почему сейчас стоит обратиться или приобрести товар — конкретно и по делу.

⚠️ За систематический спам — ограничения вплоть до блокировки и удаления тем

🔒 Предупреждение отображается только для автора темы

U

Uaromir

Гость
Есть класс ошибок, которые проходят все ручные проверки и спокойно доезжают до продакшена. Не пятисотки, не пустые поля, не сломанная структура — с этим как раз всё в порядке. Речь про случаи, когда сервис отвечает статусом 200, все поля на месте, типы правильные, и при этом одно число посчитано неверно.

Расскажу, как я проверяю такие ответы, когда в команде нет автотестов и в ближайшее время не появится: писать и поддерживать код некому, а сверка ответа с документацией делается глазами. Подход применим в любом инструменте, я просто покажу его на своём рабочем сценарии — создании заказа с расчётом скидки.

Что написано в документации

В нашей постановке блок с ценами описан так, как это обычно и выглядит в Confluence: таблица параметров, тип, обязательность и правило вычисления.

ПолеТипОбязательноеПравило
pricing.subtotalцелоедаСумма позиций без скидки
pricing.discountцелоеда10% от subtotal, округление до целого
pricing.totalцелоедаsubtotaldiscount
pricing.currencyстрокадаВсегда RUB
pricing.vatAmountцелоедаДля этого тарифа всегда 0

Ключевое здесь — третья строка. 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. Всё, что построено от факта, описывает факт, а не требование. Документация и ответ — два разных источника истины, и проверка обязана расти из первого.

article-400a0c16305e6800.png


инструмент разобрал ответ и предложил проверки по каждому полю. Обратите внимание на седьмую строку: total он предлагает сравнивать с 47 764 — то есть с текущим, неверным значением.

Поэтому сгенерированный набор — это заготовка, а не результат. Дальше её нужно править по документации, и это как раз та работа, ради которой человек в процессе и нужен.

Как выглядит проверка, построенная от документации

Из пяти полей блока pricing три проверяются тривиально, а два требуют вычисления.

Простые — это currency и vatAmount. Документация говорит «всегда RUB» и «всегда 0», значит, сравнение с константой здесь корректно: мы сверяем не с фактом, а с требованием, которое случайно совпало с фактом.

Со скидкой чуть сложнее: она не константа, а функция от суммы. Значит, subtotal нужно сначала запомнить, а discount сравнить с округлённым результатом вычисления по этому запомненному значению. И то же самое с итогом: запоминаем discount, считаем ожидаемый total как разницу и сравниваем с фактическим.

article-553d7b9183368492.png


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

Получается цепочка, которая проверяет не значения, а правило. Она останется валидной, когда изменится сумма заказа, когда поменяется состав позиций и когда придёт другой клиент. Ломаться она будет ровно в одном случае — если сервис посчитает неправильно. Это и есть то, чего мы добиваемся.

Лишние поля: почему перечислять нужные бесполезно

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

Между тем лишнее поле — это довольно серьёзно. Оно может быть внутренним флагом, случайно вылезшим наружу, отладочным значением или, что хуже, данными, которых в этом ответе быть не должно.

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

article-1258afc2ec50b4a1.png


разрешённый состав описывается по уровням вложенности. Всё, что не перечислено, считается лишним.

Куда смотреть, когда проверка покраснела

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

article-05c0e777c58c2535.png


правило видно прямо в теле ответа, рядом с проверенным полем. Красным отмечено расхождение: ожидалось

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

Но знать, что число неверное, — это полдела. Дальше нужно понять, на каком шаге операции оно испортилось: сумма пришла кривой из тарификации, скидка применилась дважды или итог пересчитали при сборке ответа.

Для этого пригодится traceId, который сервис вернул в том же ответе. По нему в хранилище логов находятся все записи операции, и по ним видно, что происходило между запросом и ответом: с какими данными вызывалась тарификация, что она вернула, каким был итог до и после применения скидки.

article-dbb01dfe0869724b.png


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

Принципиально здесь то, что это не «посмотреть логи глазами», а те же самые проверки, только по другому источнику данных. Условие «на шаге расчёта итог должен быть равен разнице» формулируется один раз и применяется и к ответу сервиса, и к записи в логе.

Что происходит на следующем релизе

Вся описанная работа заняла у меня минут пятнадцать: вставить ответ, получить заготовку, поправить четыре шага по документации, добавить проверку состава и связать с логами.

Но ценность не в первом запуске — пятнадцать минут можно и калькулятором отработать. Ценность в том, что на следующем релизе это уже не работа: открыл, нажал «запустить», получил тот же отчёт. И через месяц, когда в расчёт добавят НДС, вы поменяете одно правило вместо того, чтобы заново вспоминать, как вообще считается итог в этом сервисе.

Второй эффект — передаваемость. Проверка перестаёт жить в голове того, кто её придумал. Коллега открывает сохранённый сценарий и получает тот же результат, не задавая вопросов. Для команд, где тестирование размазано между аналитиком, разработчиком и второй линией поддержки, это часто важнее скорости.

Где этот подход не нужен

Скажу прямо, чтобы не тратить ваше время.

Если в команде уже есть автотесты, настроенный пайплайн и человек, который их поддерживает, — вам ничего из этого не нужно, задача давно решена лучше. Если проверки должны запускаться на каждый коммит, тоже мимо: описанное я делаю в настольном приложении под Windows, оно запускается руками, а не из CI. И если проверка нужна ровно один раз и повторять её никто не собирается, калькулятор быстрее.

Подход имеет смысл в другой ситуации, довольно частой: проверять API нужно регулярно, ошибки в расчётах болезненны, а кода писать некому — ни своих автотестов, ни инженера под них, ни, зачастую, разрешения отправлять реальные данные во внешние сервисы.

Если у вас есть свой способ ловить такие расхождения без написания кода — расскажите в комментариях, мне правда интересно, как это решают другие.

Источник: habr.com
 


Сверху