vyvchy
    Теми розділу

    04 · API-тестування

    SOAP, gRPC і асинхронні API

    Зміст

    REST поверх JSON — це мейнстрім, але не весь світ. У банку чи страховій ти зустрінеш SOAP, якому понад двадцять років і який нікуди не подінеться. У мікросервісній платформі два сервіси всередині часто говорять не REST, а gRPC — бінарним протоколом, який у DevTools JSON-ом не подивишся. А половина «магії» сучасних продуктів — це взагалі не запит-відповідь: платіжка сама стукає до тебе вебхуком, замовлення пливе через Kafka до складського сервісу, лист відправляється через чергу. Це асинхронний світ, де немає синхронної відповіді, яку звично заасертити, — і саме тут ламаються тести, написані за звичкою «відправив запит — перевірив тіло».

    Це глава-поглиблення: при першому проході її можна пропустити, а на співбесіді її вага низька. Але коли ти потрапляєш на проєкт із SOAP-інтеграцією, gRPC-сервісами чи чергами — тобі потрібна модель, а не паніка. Мета глави — дати чесний каркас: як влаштований кожен із цих протоколів, що саме перевіряє QA і де межа між «я це знаю» і «цим я не керував руками». Оглядову карту типів API дає перша глава розділу; тут — деталі й стратегія.

    SOAP і WSDL: чому досі живе

    SOAP — це протокол обміну структурованими повідомленнями, де кожне повідомлення загорнуте в XML-конверт (envelope) із фіксованою структурою: Envelope → опційний Header → обов'язковий Body. Зазвичай він їздить поверх HTTP методом POST, але, на відміну від REST, HTTP тут — лише транспорт: уся семантика операції живе всередині XML, а не в URL і методі.

    <soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
      <soap:Body>
        <getBalance>
          <accountId>ACC-42</accountId>
        </getBalance>
      </soap:Body>
    </soap:Envelope>

    Контракт SOAP-сервісу описує WSDL (Web Services Description Language) — машиночитний XML-документ, який перелічує операції, їхні вхідні й вихідні повідомлення та типи (через XML Schema, XSD). Для SOAP WSDL — це те саме, чим для REST є OpenAPI: єдине джерело істини про форму запитів і відповідей (пор. OpenAPI/Swagger). З WSDL інструмент може згенерувати клієнт і болванки запитів автоматично.

    Чому це досі живе, хоча «всі перейшли на REST»? Бо навколо SOAP існує зрілий стек стандартів WS-* — зокрема WS-Security (підпис і шифрування на рівні повідомлення, а не лише транспорту), формальні контракти й транзакційні розширення. Банки, страхові, державні реєстри, телеком і старі enterprise-інтеграції побудовані на цьому й переписувати їх ніхто не буде. Тому SOAP — це не «legacy, яке помирає», а «legacy, яке платить зарплати».

    Канонічний інструмент ручного й автоматизованого тестування SOAP — SoapUI (та його комерційна версія ReadyAPI): він читає WSDL, будує дерево операцій, дає слати запити й асертити відповіді за XPath. Postman теж уміє слати SOAP (це просто POST з XML-тілом і заголовком SOAPAction або відповідним Content-Type), але зручність SoapUI саме в тому, що він розуміє WSDL як контракт.

    Головна пастка для QA: SOAP-помилка — це не HTTP-помилка. Коли операція завершується невдало, сервіс повертає SOAP Fault — спеціальний елемент у Body (у SOAP 1.1 з полями faultcode/faultstring, у SOAP 1.2 перейменованими на Code/Reason). За специфікацією Fault їде зі статусом 500 (у SOAP 1.2 помилка на боці відправника — 400), але на практиці чимало реалізацій кладуть Fault у тіло навіть під 200 OK. Тобто тест, який дивиться лише на HTTP-код, легко пропустить фактичну помилку в конверті. Це той самий клас пасток, що й «200 з помилкою в тілі» у REST і масив errors у GraphQL: перевіряй тіло, а не тільки статус.

    gRPC: контракт, типи викликів, інструменти

    gRPC — це фреймворк віддалених викликів процедур (Remote Procedure Call, RPC) від Google, який працює поверх HTTP/2 і серіалізує дані у Protocol Buffers (protobuf) — компактний бінарний формат. Замість «ресурсів і методів HTTP» тут ідея інша: клієнт викликає метод сервісу так, ніби це локальна функція, а фреймворк ховає мережу під капотом.

    Контракт описується у .proto-файлі: сервіс, його методи (rpc) і структура повідомлень зі строго типізованими полями. Це схема, генератор коду і документація в одному:

    syntax = "proto3";
    
    service AccountService {
      rpc GetBalance (BalanceRequest) returns (BalanceReply);
    }
    
    message BalanceRequest {
      string account_id = 1;
    }
    
    message BalanceReply {
      int64 cents = 1;
      string currency = 2;
    }

    Числа 1, 2 біля полів — це не значення, а порядкові теги полів у бінарному кодуванні; саме тому protobuf зворотно сумісний: старе поле можна лишити, нове додати з новим тегом, і старі клієнти його просто проігнорують. Ця дисципліна тегів — основа сумісності версій (детально — у главі контрактне тестування).

    Навіщо це, якщо є простий JSON? Три причини: швидкість (бінарний формат і мультиплексування HTTP/2 — багато викликів по одному з'єднанню без блокування черги), строга типізація прямо з контракту й стримінг. gRPC має чотири типи викликів, і це часто питають:

    Тип викликуКлієнт шлеСервер відповідаєПриклад
    Unaryодне повідомленняодне повідомленнязвичайний запит балансу
    Server streamingоднепотік повідомленьпідписка на оновлення ціни
    Client streamingпотікоднезавантаження великого файлу частинами
    Bidirectional streamingпотікпотікчат, двобічна телеметрія

    Ключова відмінність для AQA: gRPC-виклик не подивишся в DevTools як JSON — це бінарь поверх HTTP/2, тож звична вкладка Network майже безпорадна (детальніше про це — у главі REST API та формати даних розділу про веб). Так само gRPC має власні статус-коди, а не HTTP: OK (0), NOT_FOUND (5), INVALID_ARGUMENT (3), UNAUTHENTICATED (16), DEADLINE_EXCEEDED (4) і так далі. Плутати gRPC-статус із HTTP-статусом — типова помилка новачка в gRPC.

    Канонічний CLI-інструмент — grpcurl: це «curl для gRPC». Щоб він знав форму викликів, йому потрібен або .proto-файл, або ввімкнена на сервері server reflection (сервіс сам віддає свою схему). Без схеми grpcurl не знає, як серіалізувати запит:

    grpcurl -plaintext -d '{"account_id": "ACC-42"}' localhost:50051 AccountService/GetBalance

    grpcurl приймає JSON на вхід і показує JSON на виході, а бінарне кодування бере на себе — тому руками ти працюєш зі зрозумілим текстом. Postman теж підтримує gRPC-запити з імпортом .proto; є й окремі GUI-клієнти. Стратегія тестування далі та сама, що й для REST: перевірки статусу (тут — gRPC-статусу), полів відповіді, меж і негативних сценаріїв, тільки контракт береться з .proto, а не з OpenAPI.

    Вебхуки: вихідні виклики і стратегія перевірки

    Досі протокол був синхронним: ти шлеш запит — одразу отримуєш відповідь. Вебхук (webhook) перевертає напрямок: це коли зовнішня система сама викликає тебе — робить HTTP-POST на твій URL, щойно стається подія. «Не дзвони нам — ми подзвонимо тобі». Платіжний провайдер стукає, коли оплата пройшла; GitHub — коли є push; сервіс розсилки — коли лист відкрито.

    Це головна складність тестування: у звичайному API-тесті ти ініціюєш запит і одразу маєш відповідь для асерту. З вебхуком ти — приймач: подію тригерить хтось інший, прилетить вона асинхронно й невідомо коли, а провайдер ще й може прислати її повторно. Класична стратегія перевірки (це канонічний виклад — інші глави посилаються сюди):

    1. Підняти приймач. Локальний HTTP-сервер, що ловить POST і зберігає тіло; для реального провайдера localhost треба виставити назовні тунелем (ngrok і аналоги). Механіка приймача й тунелів — у главі мокання залежностей.
    2. Стригерити подію — через API продукту або пісочницю провайдера (наприклад, тестова оплата).
    3. Дочекатися доставки полінгом, а не sleep-ом. Вебхук асинхронний: фіксований sleep(3000) то марно чекає, то не дочікується. Правильно — полінг із таймаутом (механіка очікувань — глава стабільність API-тестів).
    4. Перевірити те, що прилетіло: схему тіла (як і будь-яку відповідь); підпис; і — головне — побічний ефект у твоїй системі (вебхук прийшов → стан справді змінився, замовлення стало paid).

    Окремо про підпис. Вебхук приходить на публічний URL, тож будь-хто може підробити POST і сказати «оплата пройшла». Щоб цьому запобігти, провайдер підписує тіло — зазвичай HMAC (hash-based message authentication code) із спільним секретом — і кладе підпис у заголовок. Приймач має перерахувати HMAC від сирого тіла тим самим секретом і звірити; не збіглося — запит відкинути. Перевірка, що продукт валідує підпис і відкидає підроблений/зіпсований, — обов'язковий негативний тест, а не опція.

    І пам'ятай про два боки медалі. Ти можеш тестувати продукт як споживача чужих вебхуків (правильно приймаємо й обробляємо) — і як продюсера власних: чи генерується подія взагалі, чи є ретраї при недоступному отримувачі, чи правильний підпис і формат. Ретраї підводять нас до ключової теми асинхронності — дублікатів.

    Система (стан)Твій приймачПровайдерПодія (оплата)Система (стан)Твій приймачПровайдерПодія (оплата)приймач недоступний / таймаутдругий доставлений повторне має створити другий платіжсталася подіяPOST /webhook (підписаний)POST /webhook (повтор — той самий payload)перевірити підписзмінити стан → paidСистема (стан)Твій приймачПровайдерПодія (оплата)Система (стан)Твій приймачПровайдерПодія (оплата)приймач недоступний / таймаутдругий доставлений повторне має створити другий платіжсталася подіяPOST /webhook (підписаний)POST /webhook (повтор — той самий payload)перевірити підписзмінити стан → paid

    Черги повідомлень: Kafka і RabbitMQ

    Ще далі від запиту-відповіді — черги повідомлень (message queues). Продюсер кладе повідомлення в брокер і йде далі, не чекаючи; консюмер колись його забере й обробить. Це розв'язує зв'язність сервісів (вони не знають одне про одного), згладжує піки навантаження й дає стійкість: якщо консюмер лежить, повідомлення чекають у брокері, а не губляться.

    Два імена, які треба знати. RabbitMQ — класичний брокер повідомлень: продюсер шле в exchange, той маршрутизує в черги, консюмери розбирають; після підтвердження (ack) повідомлення зникає. Kafka — це радше розподілений лог: повідомлення пишуться в topic, поділений на партиції, і лишаються там на час retention; консюмери читають, рухаючи свій offset, і те саме повідомлення можуть прочитати різні групи. Для QA практична різниця в тому, що в Kafka повідомлення можна перечитати й після обробки, а в RabbitMQ після ack його вже нема.

    Що тут перевіряє тестувальник:

    • Формат повідомлення. Повідомлення має свій контракт (у Kafka часто через schema registry). Це той самий контроль схеми, що й для API-відповіді: типи, обов'язкові поля, сумісність версій.
    • Продюсер справді публікує. Дія в продукті (створили замовлення) → у топіку/черзі з'явилося очікуване повідомлення. Тут головна пастка: продюсер не отримує «відповіді», тож заасертити нема чого за звичкою — треба або читати чергу в тесті, або перевіряти наслідок.
    • Консюмер правильно обробляє. Поклали повідомлення (або стригерили подію) → перевірили побічний ефект нижче за течією (запис у БД, наступне повідомлення, надісланий лист).
    • Гарантії доставки. at-most-once (можлива втрата), at-least-once (можливі дублікати — найпоширеніше), exactly-once (складна й обмежена гарантія, часто це насправді «at-least-once + ідемпотентний консюмер», а не магія брокера). На співбесіді чесніше сказати саме так, ніж обіцяти «Kafka гарантує exactly-once» без нюансів.
    • Повторна обробка й недоставлені. Що стається, коли консюмер падає на повідомленні: брокер віддасть його знову. Отруйне повідомлення (що завжди падає) не має крутитися вічно — його відправляють у чергу недоставлених (dead letter queue, DLQ) після N спроб. Перевірити, що ретраї працюють і що DLQ ловить безнадійне, — окремий сценарій.
    • Порядок. У Kafka порядок гарантований лише в межах однієї партиції, не між ними; у розподілених системах загалом розраховувати на глобальний порядок не варто.

    Технічно локальний брокер для тестів піднімають контейнером (testcontainers і подібне — деталі поза цією главою), а тест або читає топік/чергу напряму, або асертить наслідок обробки.

    Ідемпотентність консюмера і дедуплікація

    Це серце всієї асинхронності, тож винесемо окремо. Домінантна гарантія в чергах і вебхуках — at-least-once: система радше доставить повідомлення двічі, ніж втратить його. А отже, дублікати — не аварія, а нормальний режим роботи: ретрай вебхука після таймауту, повторна доставка після падіння консюмера, повтор продюсера при мережевому збої.

    Наслідок жорсткий: консюмер зобов'язаний бути ідемпотентним. Обробка того самого повідомлення двічі має давати той самий результат, що й один раз. Інакше — подвійне списання коштів, два однакові листи, дубль замовлення. Ідемпотентність тут — точно та сама ідея, що й Idempotency-Key для вхідних POST-запитів (див. главу тест-дизайн для API), тільки з боку отримувача.

    Механізми дедуплікації, які варто впізнавати:

    • Кожне повідомлення/вебхук несе унікальний id; консюмер тримає таблицю оброблених id і мовчки ігнорує вже бачені.
    • Унікальне обмеження (unique constraint) у БД, яке фізично не дасть вставити дубль.
    • Upsert (вставити-або-оновити) замість чистого insert — повтор просто перезапише тим самим значенням.

    І звідси випливає головний тест асинхронної системи: достав те саме повідомлення (чи той самий вебхук) двічі — і переконайся, що побічний ефект стався рівно один раз. Один платіж, один лист, одне замовлення. Якщо після двох однакових доставок з'явилося два платежі — це не «баг черги», це неідемпотентний консюмер, і саме твій тест мусить його спіймати.

    Так

    Ні

    Прийшло повідомлення

    id вже оброблений?

    Проігнорувати
    ефект не повторювати

    Обробити

    Записати id як оброблений

    Побічний ефект рівно раз

    Так

    Ні

    Прийшло повідомлення

    id вже оброблений?

    Проігнорувати
    ефект не повторювати

    Обробити

    Записати id як оброблений

    Побічний ефект рівно раз

    Чесна межа знань на співбесіді

    Ця тема — саме та, де сеньйор виграє чесністю, а не бравадою. Practical-експертиза в Kafka чи SOAP-стеку — це окрема глибока спеціалізація; від кандидата на QA/AQA переважно чекають правильну модель, а не досвід адміністрування брокера. Сильна відповідь звучить так: «gRPC — бінарний RPC поверх HTTP/2, контракт у .proto, чотири типи викликів, тестую через grpcurl від контракту; але продакшн-стрімінг під навантаженням руками не гоняв». Це набагато краще за впевнене плавання в деталях, яких не знаєш.

    Погана відповідь — вдавати глибину: сказати «Kafka гарантує exactly-once» без застережень, переплутати вебхук зі стрімінгом, стверджувати, що дублікати доставки — це баг брокера. Інтерв'юер якраз і мацає, чи розумієш ти межу власного знання: чи відрізниш «я це проєктував» від «я знаю, як це влаштовано». Назвати межу вголос — ознака сеньйорності, а не слабкості.

    Типові помилки

    • Виглядає як успіх, а насправді SOAP Fault. HTTP-код 200, тест зелений — але в Body лежить faultstring з помилкою. Перевіряй наявність Fault у конверті, а не лише статус транспорту.
    • Виглядає як помилка gRPC-сервера, а насправді не подав схему. grpcurl без .proto чи server reflection не знає, як серіалізувати виклик, і падає — це не баг сервісу, а забутий контракт.
    • Виглядає як HTTP-код, а насправді gRPC-статус. NOT_FOUND у gRPC — це код 5 у власній системі статусів, а не HTTP 404; асерти на HTTP-коди для gRPC безглузді.
    • Виглядає як тест вебхука, а насправді гонка. sleep(3000) замість полінгу: подія асинхронна, тож тест то марно тупцює, то не дочікується доставки — класичний флак.
    • Виглядає як «відповіді нема — значить, ок». Продюсер поклав повідомлення в чергу й не отримав відповіді — і тест нічого не заасертив. Перевіряй, що повідомлення справді лягло або що стався наслідок.
    • Виглядає як баг: вебхук прийшов двічі. Дубль доставки при at-least-once — норма. Баг — це коли неідемпотентний консюмер зробив на ньому два платежі.
    • Виглядає як покриття, а насправді лише happy-path. Одна успішна доставка перевірена, а повтор, підроблений підпис і DLQ — ні. Для асинхронних систем саме ці сценарії найцінніші.

    Підсумок

    • HTTP тут — часто лише транспорт: у SOAP семантика в XML-конверті, у gRPC — у бінарному protobuf; помилку шукай у тілі/статусі протоколу, а не тільки в HTTP-коді.
    • gRPC = бінарний RPC поверх HTTP/2, контракт у .proto, чотири типи викликів (unary й три стрімінгові), власні статус-коди; ручний інструмент — grpcurl, і йому потрібна схема.
    • Вебхук — це вхідний виклик до тебе: підняти приймач, стригерити подію, дочекатися полінгом, перевірити підпис і побічний ефект; тестувати і як споживача, і як продюсера.
    • Асинхронна доставка майже завжди at-least-once, тож дублікати — норма, а не аварія; консюмер мусить бути ідемпотентним, а дедуплікація — через унікальний id, constraint або upsert.
    • Головний тест асинхронної системи — доставити те саме повідомлення двічі й довести, що ефект стався рівно один раз.

    Що питають на співбесіді

    • «Чим gRPC відрізняється від REST і коли його брати?» Чекають: бінарний protobuf поверх HTTP/2, строгий контракт у .proto, стрімінг, швидкість — тому популярний між мікросервісами; мінус — не подивишся як JSON у DevTools. Дивляться на розуміння компромісу, а не на завчений список.
    • «Як протестувати вебхук?» Сильна відповідь — це стратегія: приймач + тунель, тригер події, очікування полінгом, перевірка схеми, підпису й побічного ефекту. Провал — «поставлю sleep і гляну». Бонус — згадати ретраї й дублікати.
    • «Прилетів той самий вебхук двічі. Баг?» Правильно: at-least-once робить дублікати очікуваними; баг — лише якщо консюмер неідемпотентний і зробив ефект двічі. Тут перевіряють, чи не плутаєш симптом із причиною.
    • «Як тестувати систему на Kafka/RabbitMQ, якщо продюсер не дає відповіді?» Очікують: асерт наслідку нижче за течією або читання топіка/черги, перевірка формату повідомлення, ідемпотентності консюмера, поведінки при повторі й DLQ.
    • «Що знаєш про SOAP?» Для не-legacy проєкту достатньо моделі: XML-конверт, контракт WSDL, SoapUI, SOAP Fault замість HTTP-помилки. Інтерв'юер дивиться, чи впізнаєш стек, а не чи писав ти WS-Security руками — тут доречно чесно окреслити межу знань.

    Джерела