Что такое идемпотентность?

Ответ

Идемпотентность — это фундаментальное свойство операции в контексте тестирования API и распределенных систем. Операция считается идемпотентной, если многократное выполнение одной и той же операции с одинаковыми входными данными приводит к одному и тому же результату, что и однократное выполнение.

Почему это критически важно для QA-инженера?

  1. Отказоустойчивость и повторные запросы: Клиент (или тест) может безопасно повторять идемпотентный запрос при таймаутах, сетевых сбоях, не получая побочных эффектов (например, создания дубликатов).
  2. Предсказуемость тестов: Идемпотентные операции делают тесты более стабильными и воспроизводимыми.
Идемпотентность HTTP-методов (с точки зрения тестировщика): Метод Идемпотентен? Объяснение и что проверять
GET Да Получение данных. 10 одинаковых GET /users/1 должны вернуть один и тот же ответ (если данные не менялись иным способом). Проверяем кэширование.
POST Нет Создание ресурса. Повторный POST /users с телом {"name":"John"} создаст второго пользователя с тем же именем (дубликат). Это частая причина багов.
PUT Да Полное обновление. PUT /users/1 с телом {"name":"Alice"}. Сколько бы раз мы его ни отправили, в итоге у пользователя с id=1 будет имя "Alice". Проверяем, что последний запрос "побеждает".
DELETE Да Удаление. Первый DELETE /users/1 удалит пользователя и вернет 200 или 204. Последующие вызовы должны возвращать 404 (Not Found) или 410 (Gone) — результат разный, но состояние системы не меняется после первого вызова. Это все равно идемпотентность.
PATCH Нет (обычно) Частичное обновление. Зависит от реализации. PATCH /users/1 с {"balance": +10} при каждом вызове будет увеличивать баланс на 10. Это неидемпотентно.

Пример теста на проверку идемпотентности метода PUT:

// Пример на Java с RestAssured
import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;

@Test
public void testPutMethodIsIdempotent() {
    String userJson = "{"name": "TestUser", "email": "test@example.com"}";

    // 1. Первый PUT-запрос (создание/обновление)
    given()
        .contentType("application/json")
        .body(userJson)
    .when()
        .put("/api/users/123")
    .then()
        .statusCode(200) // или 201 Created
        .body("name", equalTo("TestUser"));

    // 2. Второй ИДЕНТИЧНЫЙ PUT-запрос
    given()
        .contentType("application/json")
        .body(userJson) // То же самое тело
    .when()
        .put("/api/users/123")
    .then()
        .statusCode(200) // Должен быть тот же статус, что и в первый раз (не 201)
        .body("name", equalTo("TestUser")); // Имя должно остаться "TestUser", а не измениться

    // 3. Дополнительная проверка: GET после двух PUT должен вернуть те же данные
    when()
        .get("/api/users/123")
    .then()
        .body("name", equalTo("TestUser"));
}

Как тестировать неидемпотентные операции (например, POST)?

  1. Использовать уникальные данные (timestamp, UUID) в каждом тестовом прогоне.
  2. Реализовать механизм очистки тестовых данных перед/после теста.
  3. Проверять, что сервер возвращает корректные ошибки при попытке создать дубликат (например, 409 Conflict).

Ответ 18+ 🔞

А, идемпотентность! Ну это, блядь, классика, про которую каждый второй архитектор на собеседовании спрашивает, а потом в коде проёбывает. Слушай сюда, сейчас разжую.

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

Так вот, в API та же херня. Нам, тестировщикам, это надо понимать, чтобы не охуеть от результатов, когда сеть глючит и запросы летят по три раза.

Смотри, табличка, которую надо выучить, как «Отче наш», а то будешь выглядеть как манда с ушами на ретро:

  • GET — идемпотентный. Это как спросить «который час?». Спрашивай хоть сто раз — время-то одно и то же (пока стрелки не сдвинутся, но это уже другая операция). Проверяй, что кэш не ебёт мозги.
  • POST — НЕ идемпотентный, мать его. Это как сказать «роди мне ребёнка». Скажешь два раза — получишь близнецов, скажешь пять — получишь баскетбольную команду. Самый частый источник говна в тестах — дубли из-за повторных POST. Доверия к нему — ноль ебать.
  • PUT — идемпотентный. Это как команда «поставь чашку на стол». Поставил — стоит. Ещё раз сказал «поставь чашку на стол» — она так и стоит на том же месте, не взлетает. Проверяй, что последняя команда победила, а не создала вторую чашку из воздуха.
  • DELETE — идемпотентный, но хитрый. Первый раз говоришь «выбрось чашку» — она летит в урну. Второй раз говоришь — а её уже нет. Результат разный (сначала 200 OK, потом 404), но состояние системы после первого вызова не меняется: чашки нет. Это ок.
  • PATCH — обычно НЕ идемпотентный. Это как сказать «долей в чашку воды». Скажешь раз — долил, скажешь два — перелил, скажешь три — потоп устроил. Подозрение ебать чувствую к этому методу.

Вот тебе пример, как проверить, что PUT не обманывает. Смотри код, его не трогаю, он святой.

@Test
public void testPutMethodIsIdempotent() {
    // Готовим нашего подопытного пользователя
    String userJson = "{"name": "TestUser", "email": "test@example.com"}";

    // Первый залп. Создаём/апдейтим юзера с id 123.
    given()
        .contentType("application/json")
        .body(userJson)
    .when()
        .put("/api/users/123")
    .then()
        .statusCode(200)
        .body("name", equalTo("TestUser"));

    // А теперь, внимание, хуй с горы! Шлём ТОЧНО ТАКОЙ ЖЕ запрос ещё раз.
    given()
        .contentType("application/json")
        .body(userJson)
    .when()
        .put("/api/users/123")
    .then()
        .statusCode(200) // Должен быть 200, а не 201 Created второй раз! Иначе пизда рулю.
        .body("name", equalTo("TestUser")); // Имя должно остаться TestUser, а не превратиться в TestUserUser.

    // Финальный аккорд. Дёргаем GET и убеждаемся, что всё на месте, как и должно быть.
    when()
        .get("/api/users/123")
    .then()
        .body("name", equalTo("TestUser"));
}

А что делать с этим ебанько POST, который плодит сущности как сука? Тут надо хитрить.

  1. Уникальность. Пихай в каждый тестовый запрос что-то уникальное: timestamp, случайный UUID. Типа "email": "test_" + System.currentTimeMillis() + "@example.com". Чтобы второй запрос просто не прошёл по уникальности.
  2. Уборка за собой. Перед тестом создал — после теста прибил. Или перед следующим прогоном всю базу тестовую в ноль. Чистота — залог ахуенных и стабильных тестов.
  3. Проверка на адекватность. Если всё-таки шлёшь два одинаковых POST, сервер должен тебе вежливо, но жёстко сказать: «Чувак, ты чё, такое уже есть» — и вернуть ошибку 409 Conflict. Если он вместо этого создаёт дубликат — это повод написать баг-репорт размером с небольшую повесть.

Короче, запомни: идемпотентность — это когда от твоего запроса не должно быть мультиков, как от неконтролируемого POST. Один раз — и готово. Как горох об стенку.

Видео-ответы