
Повторная отправка формы: как проверить защиту от создания двух заявок


Если пользователь нажал кнопку отправки дважды, обновил страницу или клиент повторил сетевой запрос после сбоя, проблема не в самом факте второго HTTP-запроса, а в том, появился ли второй побочный эффект. Для формы заявки это обычно означает вторую запись в системе, второе письмо менеджеру, вторую задачу в CRM или второе списание связанного лимита. Поэтому проверка защиты от дубля не заканчивается на статусе ответа: нужно доказать, что создана одна заявка, а не две.
Ниже разберем узкий сценарий с явно заданным учебным API-контрактом. Он удобен тем, что снимает двусмысленность: до теста мы договариваемся, как сервер должен вести себя при повторе того же действия, а потом проверяем сначала ответ, затем реальный след в системе.
Что именно считать защитой от дубля
В HTTP есть важное различие между методом и бизнес-операцией. RFC 9110 описывает идемпотентные методы как такие, у которых несколько одинаковых запросов имеют тот же предполагаемый эффект, что и один. Но создание новой сущности через POST по умолчанию к таким операциям не относится. Значит, для формы заявки защита от повторного создания не возникает сама собой только потому, что запрос уходит по HTTP.
Для тестировщика из этого следует практическое правило: не стоит ограничиваться проверкой интерфейса вроде «кнопка заблокировалась после первого клика». Блокировка кнопки полезна, но она только уменьшает шанс повтора на клиенте. Сам дефект может остаться на сервере: два одинаковых запроса, отправленных вручную через DevTools, Postman или автоматический ретрай клиента, все равно создадут две заявки. Поэтому узкий вопрос надо формулировать так: если сервер получает повтор одного и того же действия, сохраняет ли он одну заявку или две.
Чтобы это проверить воспроизводимо, нужен явный контракт. Без него одна команда будет ожидать второй 201 Created, другая — повтор того же идентификатора, а третья — ошибку конфликта. Такой тест быстро превращается в спор о трактовке поведения, а не в проверку качества.
Учебный API-контракт: что именно мы считаем правильным
Учебный пример. Договоримся о таком контракте для API формы:
| Ситуация | Ожидаемый ответ | Ожидаемый побочный эффект |
|---|---|---|
Первый POST /api/claims с новым Idempotency-Key | 201 Created, тело с новым requestId и deduplicated:false | Создана одна заявка |
Повтор того же запроса с тем же Idempotency-Key и тем же телом | 200 OK, тот же requestId и deduplicated:true | Новая заявка не создается |
Повтор с тем же Idempotency-Key, но измененным телом | 409 Conflict | Новая заявка не создается |
Тот же набор полей, но другой Idempotency-Key | Поведение зависит от бизнес-правил, в нашем учебном контракте это новая операция | Может быть создана еще одна заявка |
Это не универсальный стандарт для всех систем, а явно учебная договоренность для одного теста. Она полезна тем, что разделяет две разные задачи. Первая — защита от повторного выполнения одной и той же операции. Вторая — бизнес-правило «нельзя подать две похожие заявки вообще». Если не развести их заранее, легко завести ложный баг: например, в системе создается вторая заявка с новым ключом, а тестировщик считает это ошибкой, хотя продукт такого запрета не обещал.
Для нашего сценария достаточно одного наблюдаемого эффекта: в системе должна остаться ровно одна заявка на повтор одной и той же операции. Все остальные признаки — код ответа, флаг deduplicated, повтор того же requestId — помогают быстрее понять, почему сервер так сработал.
Первый запрос и повтор после задержки
Проверку удобно делать на сетевом уровне, чтобы не зависеть от поведения кнопки в интерфейсе. Среда примера — терминал с curl. Чтобы результат был воспроизводимым, для каждого прогона берите новый, ранее не использованный Idempotency-Key. В учебном кейсе ниже это qa-claim-run-001. Дополнительно полезно пометить тело уникальным маркером, например значением topic. Тогда вы не спутаете текущий запуск со старой заявкой из предыдущего теста.
curl -i -X POST http://localhost:8080/api/claims \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: qa-claim-run-001' \
-d '{
"fullName": "Иван Петров",
"phone": "+79990001122",
"topic": "demo-run-001"
}'Ожидаемый первый ответ по нашему учебному контракту:
HTTP/1.1 201 Created
Content-Type: application/json
{
"requestId": "R-1042",
"status": "accepted",
"deduplicated": false
}Через 20 секунд повторяем тот же запрос без изменений:
curl -i -X POST http://localhost:8080/api/claims \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: qa-claim-run-001' \
-d '{
"fullName": "Иван Петров",
"phone": "+79990001122",
"topic": "demo-run-001"
}'Ожидаемый повторный ответ:
HTTP/1.1 200 OK
Content-Type: application/json
{
"requestId": "R-1042",
"status": "accepted",
"deduplicated": true
}Проверяемый результат на этом шаге двоякий. Во-первых, идентификатор заявки не меняется: было R-1042, осталось R-1042. Во-вторых, сервер сам сообщает, что обработал запрос как повтор уже известной операции. Но этого все еще недостаточно: ответ мог быть «красивым», а в базе при этом появилось две строки.
Проверка побочного эффекта: заявка действительно одна
Главная часть теста — не второй статус-код, а след в системе. В учебном контракте добавим диагностический эндпоинт, который позволяет искать заявки по Idempotency-Key. Это важнее, чем фильтр только по телефону: один и тот же номер может уже встречаться в старых тестовых данных, и тогда вы не поймете, откуда взялась лишняя запись. В реальном проекте вместо такого эндпоинта это может быть админка, журнал событий, таблица в БД, очередь задач или CRM. Смысл один: после двух одинаковых отправок нужно увидеть один созданный объект именно для текущего запуска.
curl -s 'http://localhost:8080/api/debug/claims?idempotencyKey=qa-claim-run-001'Ожидаемый ответ:
{
"count": 1,
"items": [
{
"requestId": "R-1042",
"idempotencyKey": "qa-claim-run-001",
"phone": "+79990001122",
"topic": "demo-run-001"
}
]
}Здесь у теста появляется проверяемый итог: count = 1 для нового уникального ключа текущего прогона. Если API на повторе вернул тот же requestId, но в диагностике видно две записи по этому же ключу, значит защита сломана на уровне побочного эффекта. Такой дефект особенно неприятен, потому что клиентская часть может выглядеть корректно: пользователь видит один и тот же идентификатор, а внутренние системы уже получили дубль.
Если у команды нет поиска по Idempotency-Key, зафиксируйте другую воспроизводимую предусловие: очистите учебные данные перед тестом или добавьте в заявку уникальный маркер, по которому можно однозначно отфильтровать именно этот запуск. Без такой привязки число записей само по себе не доказывает наличие или отсутствие дефекта идемпотентности.
Полезно проверить и соседний симптом: не ушло ли downstream-событие дважды. Если для команды видим только факт создания заявки, этого уже достаточно для нашего узкого сценария. Но если доступны очередь или журнал интеграции, стоит убедиться, что повтор не породил вторую команду на обработку. Иначе дубль может появиться не в самой таблице заявок, а на следующем шаге процесса.
Границы контракта: что считать багом, а что другой задачей
У этого теста есть границы, и их важно не размывать. Если вы меняете Idempotency-Key, вы уже проверяете другую операцию. В нашем учебном контракте тот же набор полей с новым ключом может создать новую заявку, и это не баг защиты от повтора. Багом будет только ситуация, когда тот же ключ и то же тело создают второй объект или порождают второй побочный эффект.
Отдельная проверка — reuse ключа с другим телом. Например, во втором запросе тот же Idempotency-Key: qa-claim-run-001, но номер телефона меняется на +79990001123. Если сервер молча принимает новый вариант и создает заявку, ключ перестает обозначать одну операцию. В нашем учебном контракте это должно завершаться 409 Conflict без новой записи. Такой кейс полезен, потому что он ловит не только отсутствие дедупликации, но и путаницу в границах самой операции.
Для баг-репорта здесь удобно держать короткий шаблон:
- какой был первый запрос: URL, заголовки, тело;
- какой именно повтор выполнен: тот же ключ и то же тело или тот же ключ и другое тело;
- что вернул сервер на каждом шаге;
- какой побочный эффект найден в системе: количество заявок, идентификаторы, время создания;
- почему это расходится с согласованным контрактом.
Практический вывод для читателя такой: если вы хотите доказать, что защита от дубля работает, проверьте не только второй ответ, но и единственность созданной заявки для уникального текущего запуска. А чтобы не спорить о трактовке результата, заранее зафиксируйте контракт на повтор: тот же запрос, тот же ключ, то же ожидаемое поведение сервера и тот же наблюдаемый объект в системе.
Если хотите добрать практику именно под такие сценарии, по официальной странице школы и внутренней карточке каталога курса «Ручное тестирование (Manual QA)» подтверждаются темы тестирования форм и полей, REST, спецификации API и работы с Postman. Для задачи из этой статьи особенно полезно сочетание тест-кейсов, клиент-серверной архитектуры и API-проверок.





















