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-тесті ти ініціюєш запит і одразу маєш відповідь для асерту. З вебхуком ти — приймач: подію тригерить хтось інший, прилетить вона асинхронно й невідомо коли, а провайдер ще й може прислати її повторно. Класична стратегія перевірки (це канонічний виклад — інші глави посилаються сюди):
- Підняти приймач. Локальний HTTP-сервер, що ловить
POSTі зберігає тіло; для реального провайдера localhost треба виставити назовні тунелем (ngrok і аналоги). Механіка приймача й тунелів — у главі мокання залежностей. - Стригерити подію — через API продукту або пісочницю провайдера (наприклад, тестова оплата).
- Дочекатися доставки полінгом, а не sleep-ом. Вебхук асинхронний: фіксований
sleep(3000)то марно чекає, то не дочікується. Правильно — полінг із таймаутом (механіка очікувань — глава стабільність API-тестів). - Перевірити те, що прилетіло: схему тіла (як і будь-яку відповідь); підпис; і — головне — побічний ефект у твоїй системі (вебхук прийшов → стан справді змінився, замовлення стало
paid).
Окремо про підпис. Вебхук приходить на публічний URL, тож будь-хто може підробити POST і сказати «оплата пройшла». Щоб цьому запобігти, провайдер підписує тіло — зазвичай HMAC (hash-based message authentication code) із спільним секретом — і кладе підпис у заголовок. Приймач має перерахувати HMAC від сирого тіла тим самим секретом і звірити; не збіглося — запит відкинути. Перевірка, що продукт валідує підпис і відкидає підроблений/зіпсований, — обов'язковий негативний тест, а не опція.
І пам'ятай про два боки медалі. Ти можеш тестувати продукт як споживача чужих вебхуків (правильно приймаємо й обробляємо) — і як продюсера власних: чи генерується подія взагалі, чи є ретраї при недоступному отримувачі, чи правильний підпис і формат. Ретраї підводять нас до ключової теми асинхронності — дублікатів.
Черги повідомлень: 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 — повтор просто перезапише тим самим значенням.
І звідси випливає головний тест асинхронної системи: достав те саме повідомлення (чи той самий вебхук) двічі — і переконайся, що побічний ефект стався рівно один раз. Один платіж, один лист, одне замовлення. Якщо після двох однакових доставок з'явилося два платежі — це не «баг черги», це неідемпотентний консюмер, і саме твій тест мусить його спіймати.
Чесна межа знань на співбесіді
Ця тема — саме та, де сеньйор виграє чесністю, а не бравадою. 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 руками — тут доречно чесно окреслити межу знань.
Джерела
- gRPC — офіційна документація: Core concepts, architecture and lifecycle (чотири типи викликів, deadlines, metadata) і Status codes (повний перелік gRPC-статусів).
- Protocol Buffers — офіційна документація: синтаксис
proto3, теги полів і правила сумісності версій. - W3C SOAP Version 1.2 Part 1 (Recommendation): структура конверта (
Envelope/Header/Body) і SOAP Fault. - Apache Kafka Documentation і RabbitMQ Documentation: моделі доставки, партиції/offset проти черг/ack, семантика повторної обробки й dead letter queue.
- SoapUI Documentation (SmartBear): робота з WSDL, побудова запитів і асерти для SOAP-сервісів.
Що таке SOAP і чим він принципово відрізняється від REST?
SOAP — це протокол обміну структурованими повідомленнями, де кожне повідомлення загорнуте в XML-конверт (envelope) із фіксованою структурою Envelope → Header (опційний) → Body. Їздить він зазвичай поверх HTTP методом POST, але ключова відмінність від REST у тому, що HTTP тут — лише транспорт: уся семантика операції живе всередині XML, а не в URL і HTTP-методі. У REST ти дивишся на GET /accounts/42 і метод GET уже каже «читаю ресурс»; у SOAP і URL, і метод часто однакові для всіх операцій, а що саме викликається (getBalance, transfer) — написано в тілі конверта. Практичний наслідок для QA: контракт SOAP не читається з маршруту, його треба брати з WSDL, а помилку шукати в тілі, а не в HTTP-коді.
Що таке WSDL і навіщо він потрібен QA?
WSDL (Web Services Description Language) — це машиночитний XML-документ, який описує контракт SOAP-сервісу: перелік операцій, їхні вхідні й вихідні повідомлення та типи (через XML Schema, XSD). Для SOAP WSDL — це те саме, чим для REST є OpenAPI: єдине джерело істини про форму запитів і відповідей. З WSDL інструмент може автоматично згенерувати клієнт і болванки запитів, тож тестувальнику не треба вручну вгадувати структуру XML. Саме тому канонічний інструмент SoapUI спершу читає WSDL і будує з нього дерево операцій. Для QA це означає: не впевнений, які поля обов'язкові й що повертає операція — відкривай WSDL, а не пробуй наосліп.
Чому SOAP досі живий, хоча всі нібито перейшли на REST?
Бо навколо SOAP існує зрілий стек стандартів WS-*, якого в REST просто немає з коробки: зокрема WS-Security (підпис і шифрування на рівні самого повідомлення, а не лише транспорту), формальні контракти й транзакційні розширення. Банки, страхові, державні реєстри, телеком і старі enterprise-інтеграції побудовані на цьому багато років, і переписувати їх ніхто не буде — ризик і вартість надто великі. Тому SOAP коректніше сприймати не як «legacy, яке помирає», а як «legacy, яке платить зарплати». Для QA це значить, що на проєкті в певних доменах ти цілком реально зустрінеш SOAP, і треба мати модель, а не панікувати.
Що таке SOAP Fault і чому це головна пастка для тестувальника?
SOAP Fault — це спеціальний елемент усередині Body, який сервіс повертає, коли операція завершилася невдало (у SOAP 1.1 з полями faultcode/faultstring, у SOAP 1.2 перейменованими на Code/Reason). Пастка в тому, що SOAP-помилка — це не обов'язково HTTP-помилка: за специфікацією Fault їде зі статусом 500 (у SOAP 1.2 помилка на боці відправника — 400), але на практиці чимало реалізацій кладуть Fault у тіло навіть під 200 OK. Тобто тест, який дивиться лише на HTTP-код і бачить зелений 200, легко пропустить фактичну помилку в конверті. Це той самий клас пасток, що й «200 з помилкою в тілі» у REST або масив errors у GraphQL. Правильна перевірка — асертити відсутність елемента Fault у Body, а не тільки статус транспорту.
Що таке gRPC і на чому він працює?
gRPC — це фреймворк віддалених викликів процедур (Remote Procedure Call, RPC) від Google, який працює поверх HTTP/2 і серіалізує дані у Protocol Buffers (protobuf) — компактний бінарний формат. Модель тут не «ресурси й HTTP-методи», а виклик функції: клієнт викликає метод сервісу так, ніби це локальна функція, а фреймворк ховає мережу під капотом. Дані передаються не текстовим JSON, а бінарем, тому їх не прочитаєш очима без інструмента. Беруть gRPC переважно для зв'язку між мікросервісами, де важливі швидкість, строгий контракт і стрімінг. Для QA головне усвідомити: це інша парадигма, і звичні REST-звички (подивитись JSON у Network, асертити HTTP-код) тут не працюють напряму.
Що таке .proto-файл і чому числа біля полів — це не значення?
.proto — це файл, який описує контракт gRPC-сервісу: сам сервіс, його методи (rpc) і структуру повідомлень зі строго типізованими полями. Це одночасно схема, генератор коду й документація. Числа 1, 2 біля полів — це не значення й не дефолти, а порядкові теги полів у бінарному кодуванні: саме за тегом, а не за назвою, поле впізнається в байтах. Через це protobuf зворотно сумісний: старе поле можна лишити на його тегу, нове додати з новим тегом, і старі клієнти просто проігнорують невідомий тег. Ця дисципліна тегів — основа сумісності версій, тож змінити тег наявного поля — це ламальна зміна контракту, і саме таке має ловити контрактний тест.
Які є типи викликів у gRPC і чим вони відрізняються?
Їх чотири, і це часто питають. Unary — клієнт шле одне повідомлення й отримує одне у відповідь (звичайний запит балансу). Server streaming — клієнт шле одне, а сервер відповідає потоком повідомлень (підписка на оновлення ціни). Client streaming — клієнт шле потік, сервер відповідає одним (завантаження великого файлу частинами). Bidirectional streaming — обидва шлють потоки одночасно (чат, двобічна телеметрія). Ключове для AQA: три з чотирьох типів — стрімінгові, а це вже не звичне «запит-відповідь», яке легко заасертити одним рядком, тому стратегія перевірки стріму складніша. На співбесіді достатньо назвати всі чотири й навести приклад до кожного.
Чому gRPC-виклик не подивишся в DevTools і які в нього статус-коди?
Бо gRPC — це бінарь поверх HTTP/2, а не текстовий JSON поверх HTTP/1.1, тож звична вкладка Network майже безпорадна: замість читабельного тіла ти побачиш бінарні фрейми. Крім того, gRPC має власні статус-коди, а не HTTP: OK (0), INVALID_ARGUMENT (3), DEADLINE_EXCEEDED (4), NOT_FOUND (5), UNAUTHENTICATED (16) і так далі. Плутати gRPC-статус із HTTP-статусом — типова помилка новачка: NOT_FOUND у gRPC — це код 5 у власній системі, а не HTTP 404, тож асерт на HTTP-код для gRPC безглуздий. Тому для gRPC перевіряєш саме gRPC-статус і поля відповіді, а не HTTP-код транспорту.
Що таке grpcurl і чому йому потрібна схема?
grpcurl — це «curl для gRPC», канонічний CLI-інструмент, який дозволяє слати gRPC-виклики руками. Він приймає JSON на вхід і показує JSON на виході, а бінарне кодування бере на себе, тож ти працюєш зі зрозумілим текстом. Але щоб знати форму викликів і як серіалізувати запит, йому потрібен або .proto-файл, або ввімкнена на сервері server reflection (сервіс сам віддає свою схему). Без схеми grpcurl просто не знає структуру повідомлення й падає — і це не баг сервісу, а забутий контракт. Тому типова пастка звучить так: «виглядає як помилка gRPC-сервера, а насправді ти не подав схему».
Що таке вебхук і чому його складно тестувати?
Вебхук (webhook) перевертає звичний напрямок виклику: замість того щоб ти слав запит і отримував відповідь, зовнішня система сама викликає тебе — робить HTTP-POST на твій URL, щойно стається подія. «Не дзвони нам — ми подзвонимо тобі»: платіжка стукає, коли оплата пройшла, GitHub — коли є push. Складність тестування в тому, що ти тут приймач, а не ініціатор: подію тригерить хтось інший, прилетить вона асинхронно й невідомо коли, а провайдер ще й може прислати її повторно. Немає синхронної відповіді, яку звично заасертити одразу після запиту. Саме тому вебхуки ламають тести, написані за звичкою «відправив запит — перевірив тіло».
Як правильно протестувати вебхук?
Це питання про стратегію, і сильна відповідь — це послідовність кроків, а не «поставлю sleep і гляну». Перше: підняти приймач — локальний HTTP-сервер, що ловить POST і зберігає тіло; для реального провайдера localhost треба виставити назовні тунелем (ngrok і аналоги). Друге: стригерити подію — через API продукту або пісочницю провайдера, наприклад тестову оплату. Третє: дочекатися доставки полінгом із таймаутом, а не фіксованим sleep, бо подія асинхронна й sleep то марно чекає, то не дочікується. Четверте: перевірити те, що прилетіло — схему тіла, підпис і, головне, побічний ефект у твоїй системі (замовлення справді стало paid). Бонус на співбесіді — згадати ретраї й дублікати.
Навіщо вебхуку підпис і як його перевіряти?
Вебхук приходить на публічний URL, тож будь-хто може підробити POST і сказати «оплата пройшла» — без захисту система прийме фейкову подію на віру. Щоб цьому запобігти, провайдер підписує тіло, зазвичай через HMAC (hash-based message authentication code) зі спільним секретом, і кладе підпис у заголовок. Приймач має перерахувати HMAC від сирого тіла тим самим секретом і звірити з тим, що в заголовку; не збіглося — запит відкинути. Для QA це означає обов'язковий негативний тест: перевірити, що продукт валідує підпис і відкидає підроблений чи зіпсований запит, а не тільки що happy-path проходить. Перевірка підпису — не опція, а частина безпеки інтеграції.
Що таке черга повідомлень і яку проблему вона розв'язує?
Черга повідомлень (message queue) — це спосіб спілкування сервісів через посередника-брокер замість прямого виклику: продюсер кладе повідомлення в брокер і йде далі, не чекаючи, а консюмер колись його забере й обробить. Це розв'язує зв'язність сервісів — вони не знають одне про одного напряму, — згладжує піки навантаження й дає стійкість: якщо консюмер лежить, повідомлення чекають у брокері, а не губляться. Головна відмінність від запиту-відповіді в тому, що продюсер не отримує синхронної відповіді, тож заасертити нема чого за звичкою. Для QA це той самий асинхронний виклик, що й вебхуки: перевіряти треба наслідок нижче за течією або читати чергу напряму.
Чим Kafka відрізняється від RabbitMQ для тестувальника?
RabbitMQ — це класичний брокер повідомлень: продюсер шле в exchange, той маршрутизує в черги, консюмери розбирають, а після підтвердження (ack) повідомлення зникає. Kafka — це радше розподілений лог: повідомлення пишуться в topic, поділений на партиції, і лишаються там на час retention; консюмери читають, рухаючи свій offset, і те саме повідомлення можуть прочитати різні групи. Практична різниця для QA в тому, що в Kafka повідомлення можна перечитати й після обробки (offset відмотується назад), а в RabbitMQ після ack його вже нема. Ще нюанс порядку: у Kafka порядок гарантований лише в межах однієї партиції, не між ними, тож на глобальний порядок у розподіленій системі розраховувати не варто.
Які бувають гарантії доставки і що не так з обіцянкою exactly-once?
Гарантій три. At-most-once — можлива втрата: доставимо не більше разу, але можемо й загубити. At-least-once — можливі дублікати: доставимо щонайменше раз, але можемо й повторити; це найпоширеніший режим. Exactly-once — рівно один раз, але це складна й обмежена гарантія: часто те, що продають як exactly-once, насправді є «at-least-once + ідемпотентний консюмер», а не магія самого брокера. На співбесіді чесніше сказати саме так, ніж впевнено заявити «Kafka гарантує exactly-once» без застережень — інтерв'юер якраз і мацає, чи розумієш ти нюанс. Практичний висновок: раз домінує at-least-once, дублікати — це нормальний режим роботи, а не аварія.
Що таке dead letter queue і навіщо вона?
Dead letter queue (DLQ), черга недоставлених, — це місце, куди відправляють повідомлення, яке консюмер не зміг обробити після N спроб. Механізм потрібен через «отруйне повідомлення» (poison message) — таке, що завжди падає при обробці: якщо брокер просто віддаватиме його знову й знову, консюмер зациклиться на ньому вічно й не рухатиметься далі. DLQ розриває цей цикл: після кількох невдалих спроб повідомлення відкладають убік для розбору руками, а обробка черги йде далі. Для QA це окремий сценарій: перевірити, що ретраї справді працюють (тимчасовий збій долається повтором) і що безнадійне повідомлення після вичерпання спроб таки лягає в DLQ, а не крутиться нескінченно й не блокує чергу.
Що таке ідемпотентність консюмера і чому вона обов'язкова?
Ідемпотентність означає, що обробка того самого повідомлення двічі дає той самий результат, що й один раз. Вона обов'язкова тому, що домінантна гарантія доставки — at-least-once, тож дублікати неминучі: ретрай вебхука після таймауту, повторна доставка після падіння консюмера, повтор продюсера при мережевому збої. Якщо консюмер не ідемпотентний, наслідок жорсткий — подвійне списання коштів, два однакові листи, дубль замовлення. Це та сама ідея, що й Idempotency-Key для вхідних POST-запитів, тільки з боку отримувача. Механізми дедуплікації, які варто впізнавати: унікальний id повідомлення плюс таблиця оброблених id, унікальне обмеження (unique constraint) у БД, або upsert замість чистого insert.
Прилетів той самий вебхук двічі. Це баг?
Не обов'язково — і це улюблене питання-пастка. При гарантії at-least-once дубль доставки очікуваний: система радше доставить повідомлення двічі, ніж втратить його, тож повтор після таймауту чи падіння — норма, а не аварія. Баг тут інший: він з'являється, лише якщо консюмер неідемпотентний і на другій доставці зробив ефект удруге — два платежі, два листи, дубль замовлення. Тобто плутати симптом (дубль прилетів) із причиною (дубль спричинив другий ефект) — помилка. Правильна реакція QA: не заводити баг на сам факт повтору, а перевірити, чи побічний ефект стався рівно один раз. Якщо після двох однакових доставок з'явилося два платежі — це неідемпотентний консюмер, і саме твій тест мусить його спіймати.
Який головний тест асинхронної системи?
Достав те саме повідомлення (чи той самий вебхук) двічі — і переконайся, що побічний ефект стався рівно один раз: один платіж, один лист, одне замовлення. Це прямо випливає з того, що at-least-once робить дублікати нормою, а отже, єдиний надійний захист — ідемпотентність консюмера, і саме її треба перевіряти. Слабке покриття виглядає так: перевірили одну успішну доставку (happy-path) — і все; а повтор, підроблений підпис і DLQ лишилися непокритими, хоча для асинхронних систем саме ці сценарії найцінніші. Тому тест на подвійну доставку — не екзотика, а обов'язковий елемент. Якщо він червоний, ти знайшов не «баг черги», а неідемпотентного консюмера.
Як звучить чесна відповідь про SOAP чи Kafka на співбесіді, якщо руками ти з ними не працював?
Ця тема — саме та, де сеньйор виграє чесністю, а не бравадою: практична експертиза в Kafka чи SOAP-стеку — окрема глибока спеціалізація, і від QA/AQA переважно чекають правильну модель, а не досвід адміністрування брокера. Сильна відповідь окреслює межу вголос: «gRPC — бінарний RPC поверх HTTP/2, контракт у .proto, чотири типи викликів, тестую через grpcurl від контракту; але продакшн-стрімінг під навантаженням руками не гоняв». Це набагато краще за впевнене плавання в деталях, яких не знаєш. Погана відповідь — вдавати глибину: заявити «Kafka гарантує exactly-once» без застережень, переплутати вебхук зі стрімінгом, назвати дублікати доставки багом брокера. Інтерв'юер якраз і перевіряє, чи відрізняєш ти «я це проєктував» від «я знаю, як це влаштовано» — і назвати межу є ознакою сеньйорності, а не слабкості.
Три кейси, де ламаються тести, написані за звичкою «відправив запит — перевірив тіло»: SOAP Fault, що причаївся під зеленим 200 OK; ручний виклик gRPC через grpcurl, коли DevTools безпорадні; і головний тест асинхронної системи — подвійна доставка вебхука проти ідемпотентності консюмера. Скрізь — що дивитися і чому.
Кейс 1. SOAP Fault під 200 OK: чому зелений статус бреше
Тест шле SOAP-запит на переказ, отримує 200 OK, асерт на статус зелений — а гроші не переказалися. Причина класична: сервіс поклав помилку не в HTTP-код, а в тіло конверта, у вигляді SOAP Fault. Ось що реально прийшло:
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
<soap:Body>
<soap:Fault>
<soap:Code><soap:Value>soap:Sender</soap:Value></soap:Code>
<soap:Reason>
<soap:Text xml:lang="en">Insufficient funds on ACC-42</soap:Text>
</soap:Reason>
</soap:Fault>
</soap:Body>
</soap:Envelope>
HTTP-код — 200, а операція провалилась. Тест, який перевіряє лише статус транспорту, цього не побачить. Правильна перевірка — асертити відсутність елемента Fault у Body, а не тільки код:
import { test, expect } from '@playwright/test';
test('SOAP: успішний переказ не містить Fault у конверті', async ({ request }) => {
const res = await request.post('https://bank.example.com/soap/AccountService', {
headers: { 'Content-Type': 'application/soap+xml' },
data: transferEnvelope('ACC-42', 'ACC-7', 100),
});
// статусу замало — реалізації кладуть Fault навіть під 200
const body = await res.text();
expect(body, 'у Body лежить SOAP Fault — операція провалилась').not.toContain('<soap:Fault>');
expect(body).toContain('<transferResult>OK</transferResult>');
});
Що дивитися і чому:
200 OKу SOAP не означає «операція вдалась». За специфікацією Fault їде зі статусом500(у SOAP 1.2 помилка відправника —400), але на практиці багато реалізацій віддають Fault під200. Тому оракул успіху — це тіло конверта, а не HTTP-код.- Fault 1.1 vs 1.2 — різні поля. У SOAP 1.1 це
faultcode/faultstring, у 1.2 —Code/Reason. Асерт «немає Fault» треба звіряти з версією конкретного сервісу, інакше перевірка шукає не той елемент. - SoapUI зручніший саме тут. Він читає WSDL і дає асертити відповідь за XPath (наприклад, «елемент
Faultвідсутній»), не парсячи XML руками. Для регулярного SOAP-тестування це надійніше, ніжtoContainпо рядку. - Це той самий клас пасток, що й у REST/GraphQL. «200 з помилкою в тілі» і масив
errorsу GraphQL — рідні брати SOAP Fault: усюди перевіряй тіло, а не лише статус.
Кейс 2. grpcurl: викликати gRPC руками, коли Network безпорадна
Тобі кажуть «перевір, що AccountService/GetBalance повертає правильний баланс», але це gRPC — у DevTools замість JSON бінарні фрейми HTTP/2. Ручний інструмент тут — grpcurl, «curl для gRPC». Він приймає JSON на вхід і показує JSON на виході, а бінарне кодування бере на себе:
# зі схемою з .proto-файлу
grpcurl -plaintext -import-path ./proto -proto account.proto \
-d '{"account_id": "ACC-42"}' \
localhost:50051 AccountService/GetBalance
{
"cents": "125000",
"currency": "UAH"
}
Якщо сервер увімкнув server reflection, .proto не потрібен — сервіс сам віддасть схему, і можна спершу переглянути список методів:
grpcurl -plaintext localhost:50051 list AccountService
# AccountService.GetBalance
А ось як виглядає негативний сценарій — неіснуючий акаунт повертає gRPC-статус, а не HTTP-код:
ERROR:
Code: NotFound
Message: account ACC-999 not found
Що дивитися і чому:
- Без схеми grpcurl не працює — і це не баг сервісу. Немає ні
.proto, ні server reflection — grpcurl не знає, як серіалізувати запит, і падає. Пастка «виглядає як помилка gRPC-сервера, а насправді забув подати контракт». Code: NotFound— це gRPC-статус 5, а не HTTP 404. gRPC має власну систему кодів (OK0,INVALID_ARGUMENT3,DEADLINE_EXCEEDED4,NOT_FOUND5,UNAUTHENTICATED16). Асертити треба саме на них; асерт на HTTP-код для gRPC безглуздий.- Стратегія тесту та сама, що для REST. Перевірки статусу (тут — gRPC-статусу), полів відповіді, меж і негативних сценаріїв; змінюється лише джерело контракту —
.protoзамість OpenAPI, і бінарний транспорт, який grpcurl ховає за JSON. -plaintext— це про TLS, не про формат. Прапорець означає з'єднання без шифрування (типово для локального сервера); дані на вході все одно JSON. Не плутати з «показати бінарь як текст».
Кейс 3. Головний тест асинхронної системи: подвійна доставка проти ідемпотентності
Продукт приймає вебхук оплати від провайдера й ставить замовлення в paid. Happy-path (один вебхук → замовлення оплачене) команда вже покрила. Але домінантна гарантія — at-least-once, тож провайдер цілком може прислати ту саму подію двічі. Найцінніший тут не happy-path, а перевірка ідемпотентності: достав той самий вебхук двічі й доведи, що платіж стався рівно один раз.
import { test, expect } from '@playwright/test';
import crypto from 'node:crypto';
const SECRET = process.env.WEBHOOK_SECRET!;
function sign(body: string): string {
return crypto.createHmac('sha256', SECRET).update(body).digest('hex');
}
// полінг наслідку замість sleep — подія асинхронна
async function waitForOrderPaid(request, orderId: string, timeoutMs = 10_000) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const res = await request.get(`/api/orders/${orderId}`);
if ((await res.json()).status === 'paid') return;
await new Promise((r) => setTimeout(r, 300));
}
throw new Error(`замовлення ${orderId} не стало paid за ${timeoutMs}ms`);
}
test('дубль вебхука не створює другий платіж (ідемпотентність консюмера)', async ({ request }) => {
const orderId = `ord-${Date.now()}`;
// однакове тіло з однаковим event id — саме той дубль, який шле провайдер при ретраї
const body = JSON.stringify({ event_id: 'evt-abc-123', order_id: orderId, type: 'payment.succeeded' });
const headers = { 'X-Signature': sign(body), 'Content-Type': 'application/json' };
// перша доставка
const first = await request.post('/webhooks/payments', { headers, data: body });
expect(first.status()).toBe(200);
await waitForOrderPaid(request, orderId);
// друга доставка — той самий payload, той самий підпис
const second = await request.post('/webhooks/payments', { headers, data: body });
expect(second.status(), 'повтор має прийматись штатно, а не 500').toBe(200);
// головний асерт: ефект стався РІВНО один раз
const payments = await (await request.get(`/api/orders/${orderId}/payments`)).json();
expect(payments, 'два платежі = неідемпотентний консюмер').toHaveLength(1);
});
І обов'язковий негативний тест на підпис — підроблений POST має бути відкинутий:
test('вебхук з невалідним підписом відкидається', async ({ request }) => {
const body = JSON.stringify({ event_id: 'evt-x', order_id: 'ord-x', type: 'payment.succeeded' });
const res = await request.post('/webhooks/payments', {
headers: { 'X-Signature': 'deadbeef', 'Content-Type': 'application/json' },
data: body,
});
expect(res.status(), 'підроблений підпис має відхилятись').toBe(401);
});
Що дивитися і чому:
- Оракул — побічний ефект, а не відповідь на
POST. Вебхук приймається асинхронно, тож200на самPOSTще нічого не доводить. Перевіряти треба стан у системі:payments.length === 1після двох доставок. - Полінг, а не
sleep.waitForOrderPaidтисне доти, доки стан не зміниться, або падає по таймауту. Фіксованийsleep(3000)то марно чекав би, то не дочікувався — класичний флак асинхронних тестів. - Однаковий
event_id— суть перевірки. Дедуплікація тримається на унікальному id події: другийPOSTіз тим самимevent_idконсюмер має мовчки проігнорувати. Якщо змінити id, ти тестуєш дві різні події, а не ідемпотентність. - Дубль — не баг, неідемпотентність — баг. Якщо після двох однакових доставок з'явилося два платежі, це не «баг черги/провайдера», а неідемпотентний консюмер. Саме цей тест мусить його спіймати — happy-path його не бачить.
SOAP і WSDL
- Можу пояснити, що SOAP — це XML-конверт (
Envelope→Header→Body) поверх HTTP, де HTTP лише транспорт, а вся семантика операції в тілі, а не в URL/методі (у REST — навпаки: операція читається з маршруту й методу). - Розумію, що WSDL для SOAP — це те саме, чим OpenAPI для REST: машиночитний контракт з операціями, повідомленнями й типами (XSD).
- Знаю, що SOAP Fault — це елемент у
Body, а не обов'язково HTTP-помилка: реалізації часто кладуть Fault навіть під200 OK, тож перевіряти треба тіло, а не лише статус. - Можу пояснити, чому SOAP досі живий (стек WS-*, зокрема WS-Security на рівні повідомлення) і що канонічний інструмент — SoapUI/ReadyAPI (читає WSDL, асертить відповідь за XPath).
gRPC і protobuf
- Можу пояснити, що gRPC — це RPC-фреймворк поверх HTTP/2 із серіалізацією в бінарний protobuf, де клієнт викликає метод сервісу як локальну функцію, а контракт живе в
.proto(сервіс,rpc-методи, типізовані повідомлення). - Розумію, що числа біля полів — це теги в бінарному кодуванні, а не значення, і саме вони дають зворотну сумісність protobuf.
- Знаю чотири типи викликів (unary, server streaming, client streaming, bidirectional streaming) і приклад до кожного.
- Розумію, чому gRPC-виклик не подивишся в DevTools як JSON (бінарь поверх HTTP/2) і що в нього власні статус-коди, а не HTTP (
NOT_FOUND— це код 5, а не HTTP 404); grpcurl потребує.protoабо server reflection, інакше не серіалізує запит.
Вебхуки
- Можу пояснити, що вебхук перевертає напрямок: зовнішня система сама робить
POSTна твій URL при події («не дзвони нам — ми подзвонимо тобі»). - Знаю стратегію тесту вебхука: підняти приймач + тунель → стригерити подію → дочекатися полінгом (не
sleep) → перевірити схему, підпис і побічний ефект;sleep(3000)замість полінгу дає флак. - Можу пояснити, навіщо підпис (HMAC зі спільним секретом) і що перевірка відкидання підробленого/зіпсованого підпису — обов'язковий негативний тест.
- Розрізняю тестування вебхуків як споживача (правильно приймаємо/обробляємо) і як продюсера (чи генерується подія, ретраї, підпис, формат).
Черги повідомлень
- Знаю різницю Kafka vs RabbitMQ: Kafka — розподілений лог (topic/партиції/offset, повідомлення лишаються на retention, можна перечитати), RabbitMQ — черга з ack (після ack повідомлення нема).
- Розумію, що продюсер не дає синхронної відповіді, тож асертити треба наслідок нижче за течією або читати топік/чергу напряму.
- Знаю три гарантії доставки (at-most-once, at-least-once, exactly-once) і чому exactly-once часто насправді «at-least-once + ідемпотентний консюмер».
- Можу пояснити DLQ (отруйне повідомлення після N спроб іде вбік, а не крутиться вічно й не блокує чергу) і межі порядку (у Kafka порядок гарантований лише в межах партиції, не між ними).
Ідемпотентність і дедуплікація
- Розумію, чому at-least-once робить дублікати нормою й що консюмер зобов'язаний бути ідемпотентним (інакше — подвійне списання, два листи, дубль замовлення); механізми дедуплікації — унікальний
id+ таблиця оброблених, unique constraint у БД, upsert замість чистого insert. - Пам'ятаю головний тест асинхронної системи: достав те саме повідомлення двічі й доведи, що ефект стався рівно один раз; «вебхук прийшов двічі» — не баг, баг — коли неідемпотентний консюмер зробив ефект двічі (симптом ≠ причина).
Межа знань на співбесіді
- Можу назвати межу вголос: «модель знаю, продакшн-стрімінг/адміністрування брокера руками не гоняв» — і розумію, що це ознака сеньйорності, а не слабкості.
- Знаю типові провальні відповіді: «Kafka гарантує exactly-once» без застережень, плутанина вебхука зі стрімінгом, дублікати доставки як «баг брокера».
Яку роль відіграє HTTP у SOAP-запиті?
Питання
SOAP — що це і де живе семантика операції?