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-тілом і правильним Content-Type), але зручність SoapUI саме в тому, що він розуміє WSDL як контракт. Одна деталь, на якій легко спіткнутися: заголовок SOAPAction — це версія 1.1. У SOAP 1.2 такого заголовка немає: дія передається параметром медіа-типу — необовʼязковим параметром action у application/soap+xml, який використовують для оптимізації диспетчеризації чи маршрутизації.

    Головна пастка для 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, який за замовчуванням використовує 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 біля полів — це не значення, а номери полів (field numbers). Головне правило про них таке: номер не можна перепризначати чи перевикористовувати, а видаляючи поле, його номер треба зарезервувати, щоб ніхто випадково не зайняв його знову. Сумісність protobuf дає в обидва боки — і зворотну, і пряму, — але не автоматично, а «якщо дотримуватися кількох простих практик» під час оновлення .proto: тоді старий код читає нові повідомлення, просто ігноруючи нові поля. Ця дисципліна номерів — основа сумісності версій (детально — у главі контрактне тестування).

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

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

    Ключова відмінність для AQA: gRPC-виклик не подивишся в DevTools як JSON — це бінарний формат, тож звична вкладка 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-відповіді: типи, обов'язкові поля, сумісність версій.
    • Продюсер справді публікує. Дія в продукті (створили замовлення) → у топіку/черзі з'явилося очікуване повідомлення. Тут головна пастка: продюсер не отримує «відповіді», тож заасертити нема чого за звичкою — треба або читати чергу в тесті, або перевіряти наслідок.
    • Консюмер правильно обробляє. Поклали повідомлення (або стригерили подію) → перевірили побічний ефект (downstream): запис у БД, наступне повідомлення, надісланий лист.
    • Гарантії доставки. at-most-once (можлива втрата), at-least-once (можливі дублікати — задокументований дефолт Kafka), exactly-once (складна й обмежена гарантія, часто це насправді «at-least-once + ідемпотентний консюмер», а не магія брокера). На співбесіді чесніше сказати саме так, ніж обіцяти «Kafka гарантує exactly-once» без нюансів.
    • Повторна обробка й недоставлені. Що стається, коли консюмер падає на повідомленні: брокер віддасть його знову. poison message (повідомлення, що завжди падає) не має крутитися вічно — його відправляють у (dead letter queue, DLQ). Але «вичерпав N спроб» — лише один із чотирьох шляхів туди, і в RabbitMQ він привʼязаний саме до quorum-черг (перевищено delivery-limit). Решта три: консюмер зробив negative acknowledgement із requeue=false; сплив власний TTL повідомлення; чергу переповнено й повідомлення відкинуто за лімітом довжини. І окрема пастка, яка підриває інваріант «у DLQ впаде все безнадійне»: якщо спливає вся черга, її повідомлення в dead letter не потрапляють — тож порожня DLQ ще не доводить, що втрат не було. Кожен із цих шляхів — окремий сценарій.
    • Порядок. У Kafka порядок гарантований лише в межах однієї партиції, не між ними; у розподілених системах загалом розраховувати на глобальний порядок не варто.

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

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

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

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

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

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

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

    Так

    Ні

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

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

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

    Обробити

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

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

    Так

    Ні

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

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

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

    Обробити

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

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

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

    Ця тема — саме та, де сеньйор виграє чесністю, а не бравадою. Практична експертиза в Kafka чи SOAP-стеку — це окрема глибока спеціалізація; від кандидата на QA/AQA переважно чекають правильну модель, а не досвід адміністрування брокера. Сильна відповідь звучить так: «gRPC — бінарний RPC, за замовчуванням із protobuf, контракт у .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 — норма. Баг — це коли неідемпотентний консюмер зробив на ньому два платежі.
    • Виглядає як , а насправді лише . Одна успішна доставка перевірена, а повтор, підроблений підпис і DLQ — ні. Для асинхронних систем саме ці сценарії найцінніші.

    Підсумок

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

    Можливі питання

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

    Джерела

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

    • W3C — SOAP Version 1.2 Part 1: Messaging Framework — конверт Envelope з необовʼязковим Header і обовʼязковим Body; помилка живе всередині повідомлення, а HTTP — лише один із біндингів.
    • SoapUI — SOAP and WSDL: Getting Started (SmartBear) — інструмент читає WSDL і генерує запити за замовчуванням, дає асерти на кроці SOAP Request, покриття за WSDL і прямо з контракту.

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

    • gRPC — Core concepts, architecture and lifecycle — чотири типи викликів і їхні гарантії порядку; відправна точка — опис у .proto, а protocol buffers є дефолтом, а не властивістю протоколу.
    • Protocol Buffers — Overview — сумісність тримається на номерах полів, а не іменах: номер не можна перевикористати, а видаливши поле, його номер СЛІД зарезервувати.
    • gRPC — Status codes and their use in gRPC — власна система статусів: OK = 0, INVALID_ARGUMENT = 3, NOT_FOUND = 5, UNAUTHENTICATED = 16 — і вона не збігається з HTTP.
    • gRPC over HTTP2 — протокольна специфікація — на дроті кожен виклик — POST поверх HTTP/2, і успіх, і помилка їдуть із :status 200, а вердикт лежить у трейлері grpc-status.

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

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

    • Apache Kafka — Introduction (документація 4.0) — події зберігаються в партиційованих топіках і не видаляються після споживання; порядок гарантований лише в межах топік-партиції.
    • RabbitMQ — Queues (офіційна документація) — черга — FIFO зі споживанням із голови; порядок ламають кілька каналів, пріоритети й повторна доставка, а сам повтор позначено прапорцем redelivered.
    • Apache Kafka — Design (документація 4.3) — три семантики доставки дослівно, поділ на довговічність публікації й гарантії споживання, і дрібний шрифт під обіцянкою exactly-once.
    • RabbitMQ — Consumer Acknowledgements and Publisher Confirms — автоматичне підтвердження («fire-and-forget») втрачає повідомлення при закритті каналу; при ручному — непідтверджена доставка повертається в чергу.
    • RabbitMQ — Dead Letter Exchanges — чотири події, за яких повідомлення перепубліковується в dead letter, і виняток: якщо спливає вся черга, її повідомлення туди не потрапляють.

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

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

    • Apache Kafka — Design (документація 4.3) — чому «Kafka гарантує exactly-once» без застережень хибно: обіцянка часто не поширюється на падіння консюмера чи продюсера й на кілька процесів-консюмерів.

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

    Пояснення

    «Поясни» працює з власним API-ключем Anthropic: запит іде з вашого браузера прямо до Anthropic.

    Свій API-ключ (BYOK)

    Вставте власний ключ Anthropic — пояснення працюватиме на реальній моделі. Ключ зберігається лише у вашому браузері (localStorage), нікуди не надсилається, крім api.anthropic.com, і не логується.