Тестування мікросервісів: стратегія
Зміст
Ти прийшов на проєкт, де замість одного застосунку — сорок сервісів: окремі репозиторії, окремі бази даних, окремі команди, кожна деплоїть свій шматок по десять разів на день. Ти пишеш звичний наскрізний тест «користувач оформлює замовлення», і він падає щоранку — то один сервіс лежить, то другий викотив несумісну версію, то повідомлення в черзі ще не доїхало. Питання «як це взагалі тестувати, щоб не з'їхати з глузду» — і є темою цієї глави.
Це глава-поглиблення: якщо ти вперше проходиш розділ про автоматизацію, її можна пропустити й повернутися пізніше — вона спирається на піраміду тестування, тестові дублери й контрактні тести, які краще спершу зрозуміти окремо. Але для senior-рівня це обов'язкова картина світу: стратегію покриття розподіленої системи запитують саме тоді, коли перевіряють, чи ти мислиш архітектурою, а не окремим тестом. Тут ми не переказуємо механіку контрактних тестів чи трасування (для них є свої глави) — ми збираємо їх в одну стратегію: що, на якому рівні й чому.
Моноліт vs мікросервіси очима QA
Моноліт (monolith) — це один застосунок, який деплоять цілком. Виклики між його частинами відбуваються всередині одного процесу — це звичайні виклики функцій: миттєві, надійні, або впали разом з усім процесом. Дані зазвичай лежать в одній базі, тож цілісність гарантує транзакція. Для QA це зручно: є одне середовище, яке цілком реальне, один деплой, одна версія. Наскрізний тест через UI чи API перевіряє систему такою, якою вона поїде в прод.
Мікросервіси (microservices) розбивають цю монолітну брилу на багато незалежних сервісів. Кожен має власну кодову базу, власну базу даних і власний цикл релізу. Спілкуються вони вже через мережу — синхронно (HTTP/gRPC) або асинхронно (черги повідомлень, події). Головна зміна для тестувальника: поведінка системи більше не «живе» в одному місці — вона виникає з взаємодій. Замовлення проходить через сервіс кошика, сервіс оплати, сервіс складу, сервіс нотифікацій, і кожен стик між ними — це мережа, з усіма її радощами: затримки, таймаути, часткові відмови, розсинхрон версій.
| Аспект | Моноліт | Мікросервіси |
|---|---|---|
| Виклик між компонентами | Виклик функції в процесі | Мережевий запит |
| Дані | Одна база, транзакції | База на сервіс, eventual consistency |
| Деплой | Один, цілком | Незалежний на кожен сервіс |
| «Реальне» середовище | Одне, повне | Немає одного повного й стабільного |
| Де живе баг інтеграції | Всередині коду | На стиках, у мережі |
| Локалізація падіння | Один стек-трейс | Треба знайти винний сервіс |
Ключовий висновок: у моноліті найдорожчий ризик — логіка всередині; у мікросервісах він зміщується на межі між сервісами. Саме туди має дивитися стратегія тестування.
Чому e2e в мікросервісній архітектурі болить
Наскрізний (end-to-end, e2e) тест проганяє реальний користувацький сценарій через усі сервіси одразу. У моноліті це нормальний основний інструмент. У мікросервісах — джерело постійного болю, і ось чому.
Крихкість множиться. Якщо сценарій зачіпає вісім сервісів і кожен окремо доступний у 99% часу, спільна доступність усього ланцюга — це вже приблизно 0.99^8, тобто близько 92%. Приблизно кожен тринадцятий прогін червоний не через баг, а тому що якийсь один сервіс саме перевантажений, редеплоїться чи моргнув мережею. Тест перетворюється на генератор шуму.
Флак накопичується. Кожен мережевий стик додає асинхронність, таймаути й eventual consistency (кінцева узгодженість — стан, коли після запису дані з'являються в іншому сервісі не миттєво, а «колись скоро»). Один e2e-тест успадковує флак усіх ланок разом — детальніше причини й лікування розбирає глава Флакі-тести.
Незрозуміло, хто винен. Тест «оформлення замовлення» впав. У якому з восьми сервісів? Червоний e2e показує лише, що ланцюг розірвався, але не де саме. Без трасування діагностика перетворюється на розкопки по логах кількох команд.
Проблема середовища й версій. Щоб e2e був чесним, усі сервіси мають бути піднятими й у сумісних версіях одночасно. Але сервіс оплати вже викотив v5, а сервіс складу ще на v4 — на спільному стенді ти ловиш розсинхрон версій (version skew), якого в проді ніколи не буде. Чиє це середовище, хто відповідає за його цілісність — окреме організаційне питання без простої відповіді.
Повільний фідбек. Підняти півсистеми, засідити дані в кількох базах, дочекатися асинхронних ефектів — це хвилини на тест. Наскрізний набір з сотень таких тестів не влазить у швидкий цикл PR.
Кожна стрілка — мережевий стик, який може моргнути, протаймаутити чи віддати несумісну відповідь. Тест зелений, лише коли зелений увесь ланцюг. Звідси головна ідея стратегії: e2e не викорінюють, його стоншують — лишають тонкий шар для кількох критичних наскрізних шляхів, а решту впевненості добувають дешевше й нижче.
Сервіс в ізоляції vs інтеграційне середовище
Між «протестувати один сервіс окремо» і «підняти все разом» лежить головний компроміс мікросервісного тестування.
Сервіс в ізоляції — це коли ти піднімаєш один сервіс, а всіх його сусідів підміняєш дублерами (стаби, mock-сервери на кшталт WireMock чи msw). Такий тест часто називають компонентним (component test): він перевіряє сервіс цілком, від його API до його бази, але без справжніх сусідів. Він швидкий, детермінований, повністю під твоїм контролем — ти сам вирішуєш, що відповість сервіс оплати, зокрема відтворюєш його помилки й таймаути, які на живому стенді не спровокуєш. Таксономію дублерів (stub, mock, fake, spy) розбирає окрема глава Тестові дублери.
Ціна ізоляції: твій стаб — це твоє уявлення про сусіда, а не сам сусід. Якщо сервіс оплати змінив формат відповіді, а твій стаб — ні, компонентний тест лишається зеленим, тоді як у проді все зламано. Ізоляція не доводить, що сервіси реально домовляються.
Інтеграційне середовище — це коли справжні сервіси підняті й з'єднані між собою (весь стенд або його зв'язний шматок). Тут інтеграція справжня: реальні формати, реальна мережа, реальна асинхронність. Але за це платиш усім, за що болить e2e: повільно, флакі, спільний стенд, який хтось постійно ламає, складний сідинг даних одразу в кілька баз.
| Сервіс в ізоляції | Інтеграційне середовище | |
|---|---|---|
| Що піднято | Один сервіс + дублери | Реальні сервіси разом |
| Швидкість | Висока | Низька |
| Детермінованість | Висока (ти керуєш дублерами) | Низька (мережа, асинхронність) |
| Що ловить | Логіку сервісу, обробку помилок сусідів | Реальні розбіжності інтеграції |
| Чого не ловить | Розбіжність стаба з реальним сусідом | Тоне у флаку й вартості |
| Хто «власник» | Команда сервісу | Спільний, розмитий |
Між цими двома полюсами зяє прогалина: ізоляція швидка, але не бачить реальних розбіжностей на стиках; інтеграція бачить, але надто дорога, щоб покривати нею все. Саме цю прогалину закривають контрактні тести.
Місце контрактних тестів
Контрактний тест (contract testing) перевіряє, що двоє сервісів згодні щодо формату їхньої взаємодії — без того, щоб піднімати обидва одночасно. Контракт (contract) — це зафіксована спільна очікуваність: споживач (consumer) заявляє, які поля й у якому вигляді він чекає від постачальника (provider), а постачальник окремо перевіряється проти цієї заяви.
У підході consumer-driven contracts (контракти, керовані споживачем; інструмент-еталон — Pact) це працює так: тест споживача піднімає мок постачальника й водночас записує очікування у файл-контракт. Постачальник потім у своєму пайплайні програє цей контракт проти себе справжнього й перевіряє, що досі його виконує. Обидва тести — швидкі й ізольовані, але разом вони дають гарантію, якої не дає жодна ізоляція окремо: що стаб споживача не розійшовся з реальним постачальником.
Це і є та сама втрачена ланка. Контракт ловить те, що пропускає ізоляція (розсинхрон формату), не платячи ціною інтеграційного стенду. Механіку — schema drift, breaking vs non-breaking зміни, Pact Broker, валідацію проти OpenAPI як полегшений контракт — детально розбирає глава про контрактне тестування в розділі API-тестування; тут важлива її роль у стратегії: контрактні тести замінюють значну частину інтеграційних e2e-перевірок «чи домовляються сервіси».
Важлива межа: контракт перевіряє форму взаємодії (поля, типи, статуси), а не бізнес-результат наскрізного сценарію. Він скаже «сервіс оплати досі повертає поле status», але не скаже «замовлення справді оплатилося й склад його зарезервував». Тому контракти не витісняють тонкий шар e2e — вони прибирають потребу ганяти повний ланцюг лише заради перевірки сумісності API.
Distributed tracing (оглядово)
Коли інтеграційний чи e2e-тест таки впав, залишається найдорожче питання: у якому із сервісів розірвався ланцюг? Відповідь дає distributed tracing (розподілене трасування) — техніка, що зшиває один запит через усі сервіси в єдину картину.
Ідея проста. На вході в систему запиту присвоюють унікальний ідентифікатор — trace id (або correlation id). Цей ідентифікатор передається далі з кожним міжсервісним викликом через спеціальний заголовок (стандарт W3C Trace Context визначає для цього заголовок traceparent). Кожен сервіс логує свій відрізок роботи як span (проміжок) з тим самим trace id. Зібравши всі span за одним trace id, система показує повний шлях запиту: через які сервіси пройшов, скільки часу провів у кожному, де саме впав чи затупив.
Для QA це прямий інструмент, а не абстракція для SRE:
- Локалізація падіння. Trace id з упалого тесту веде прямо до сервісу-винуватця замість ручного зіставлення логів кількох команд.
- Атач до баг-репорту. Trace id у баг-репорті економить розробнику години — він одразу бачить повний шлях запиту.
- Розуміння асинхронності. Трейс показує, де запит чекав на чергу чи зовнішнє API — часто це і є причина «плаваючого» таймауту в тесті.
Стандарт-де-факто для збору телеметрії — OpenTelemetry (проєкт CNCF); дивляться трейси у застосунках на кшталт Jaeger чи Zipkin. Механіку роботи з логами й трасуванням під час діагностики докладніше розбирає окрема глава про спостережуваність у розділі про Git і CI/CD; performance-кут (APM, розподілене трасування під навантаженням) — у розділі про performance-тестування. Тобі як QA достатньо знати: якщо система інструментована трасуванням, вимагай доступу до неї — це найшвидший шлях від червоного тесту до винного сервісу.
Стратегія покриття системи за рівнями
Тепер зберемо все в одну стратегію. Принцип той самий, що й у піраміди тестування — штовхай перевірку якнайнижче, — але для мікросервісів піраміда набуває характерної форми, яку часто малюють як «стільник» (honeycomb): вузький верх e2e, широкий середній шар інтеграційних і компонентних тестів, вузький низ юніт-тестів (їх пише команда сервісу, і на рівні системи вони вже деталь реалізації).
Як розкласти покриття по рівнях:
- Юніт — логіка всередині сервісу; відповідальність команди сервісу.
- Компонентні — сервіс в ізоляції: його API, його база, обробка помилок і таймаутів сусідів (сусіди — дублери). Тут основна маса перевірок поведінки сервісу.
- Контрактні — сумісність кожної пари «споживач ↔ постачальник». Замінюють більшість інтеграційних перевірок «чи збігаються формати».
- Інтеграційні — реальні стики там, де контракт недостатній: асинхронні потоки через черги, складні багатокрокові взаємодії. Обмежено, бо дорого.
- E2E — лише кілька найкритичніших наскрізних бізнес-шляхів (оформлення замовлення, реєстрація, оплата). Це смоук довіри до системи в цілому, а не спосіб покрити всі гілки.
Правило пріоритету при виборі рівня: перш ніж писати наскрізний тест, спитай — чи не ловиться цей ризик дешевше нижче? Розбіжність формату → контракт. Логіка обробки помилки сусіда → компонентний тест з дублером. І лише «чи справді весь бізнес-сценарій проходить наживо» → тонкий e2e. Ізоляцію даних для всього цього (окремий tenant, унікальні дані на прогін) розбирає глава Тест-дані — у розподіленій системі вона критична вдвічі, бо стан розмазаний по кількох базах.
Типові помилки
Виглядає як «покриємо все наскрізними тестами — це найреалістичніше», а насправді — це найдорожчий і найкрихкіший спосіб покрити те, що дешево ловиться нижче. Наскрізний набір на сотні кейсів у мікросервісах не тримається: 0.99^N доступності перетворює його на генератор червоного шуму, а не в сигнал.
Виглядає як «контрактний тест — це різновид інтеграційного», а насправді — ні. Інтеграційний піднімає обидва сервіси разом; контрактний перевіряє кожен окремо проти зафіксованого очікування й не потребує їх одночасно. Плутанина веде до того, що команда або дублює роботу, або взагалі не робить контрактів, покладаючись на крихкий інтеграційний стенд.
Виглядає як «зелено в ізоляції — значить, працює в проді», а насправді — компонентний тест зелений рівно доти, доки твій стаб збігається з реальним сусідом. Без контракту стаб тихо застаріває, і в проді ловиш розсинхрон, якого жоден ізольований тест не бачив.
Виглядає як «мок сусіда = реальна поведінка сусіда», а насправді — мок повертає те, що ти в нього заклав. Він не знає про новий обов'язковий заголовок, зміну коду помилки чи новий формат дати. Мок — гіпотеза про сусіда, і без контракту ніхто не перевіряє, що гіпотеза досі правдива.
Виглядає як «тест червоний — значить, баг у продукті», а насправді — у розподіленій системі червоний e2e найчастіше означає моргання середовища, розсинхрон версій на спільному стенді чи ще-не-доїхало-повідомлення. Спершу — трасування й перевірка версій сервісів, і лише потім висновок про баг.
Підсумок
- У мікросервісах ризик зміщується з логіки всередині сервісу на стики між сервісами — туди й має дивитися стратегія тестування.
- Наскрізні e2e не викорінюють, а стоншують до кількох критичних бізнес-шляхів:
0.99^Nдоступності робить великий e2e-набір ненадійним. - Сервіс в ізоляції швидкий, але не бачить реальних розбіжностей; інтеграційне середовище бачить, але дороге — прогалину між ними закривають контрактні тести.
- Контракт перевіряє форму взаємодії, а не бізнес-результат; він замінює інтеграційні перевірки сумісності, але не тонкий шар e2e.
- Distributed tracing через єдиний trace id — головний інструмент QA для локалізації, у якому сервісі розірвався ланцюг.
Що питають на співбесіді
- «Чим тестування мікросервісів відрізняється від моноліту?» — інтерв'юер перевіряє, чи бачиш ти зсув ризику на мережеві стики, незалежні деплої й відсутність одного повного середовища, а не переказуєш означення.
- «Чому не покрити все наскрізними тестами?» — чекають на крихкість (
0.99^N), вартість, флак, розсинхрон версій і складність локалізації падіння; сильна відповідь одразу пропонує альтернативу — стоншити e2e й штовхнути перевірки нижче. - «Де місце контрактних тестів і чим вони кращі за інтеграційні?» — тут дивляться, чи розумієш ти, що контракт ловить розбіжність формату дешево й без спільного стенду, і чітко відділяєш «форму взаємодії» від «бізнес-результату».
- «Тест оформлення замовлення впав — з чого почнеш?» — очікують систематику: перевірити, чи всі сервіси підняті й у сумісних версіях, узяти trace id і за трасуванням знайти винний сервіс, і лише тоді вирішувати «баг продукту чи проблема середовища».
- «Намалюй стратегію покриття для системи з N сервісів» — сильний кандидат малює стільник/піраміду з рівнями (юніт → компонентні → контрактні → інтеграційні → тонкий e2e) і обґрунтовує, що на якому рівні ловиться.
Джерела
- Toby Clemson. Testing Strategies in a Microservice Architecture — martinfowler.com (канонічний огляд рівнів: unit, component, integration, contract, e2e).
- Pact — офіційна документація consumer-driven contract testing (docs.pact.io).
- W3C Trace Context — специфікація заголовка
traceparentдля передавання контексту трасування (w3.org). - OpenTelemetry — офіційна документація зі збору трейсів і span (opentelemetry.io).
- ISTQB CTFL 4.0 Syllabus — розділ «Testing Throughout the Software Development Lifecycle» (рівні тестування: component integration та system integration testing).
Чим тестування мікросервісів відрізняється від моноліту з погляду QA?
У моноліті вся поведінка живе в одному процесі: компоненти викликають одне одного як звичайні функції, дані лежать в одній базі під транзакцією, деплой один. Тому найдорожчий ризик — це логіка всередині коду, а перевіряти її зручно на єдиному повному середовищі. У мікросервісах систему розрізано на десятки незалежних сервісів із власними базами й окремими релізами, а спілкуються вони через мережу. Через це поведінка більше не зосереджена в одному місці — вона народжується зі взаємодій сервісів. Головний практичний наслідок: ризик зсувається з «що всередині» на «що на стиках», а разом із ним зникає одне повне й стабільне середовище, на якому можна було б усе прогнати.
Що таке наскрізний (e2e) тест і чому в мікросервісах він перетворюється на біль?
Наскрізний тест ганяє справжній користувацький сценарій через усю систему одразу — від UI чи вхідного API до останнього сервісу в ланцюгу. У моноліті це природний основний інструмент, бо «вся система» — це один застосунок. У розподіленій архітектурі той самий сценарій зачіпає багато сервісів, і кожен стик між ними — це мережа з таймаутами, чергами й частковими відмовами. Тест стає зеленим лише тоді, коли одночасно здорові всі ланки ланцюга, тому він падає не через баг, а через моргання середовища. Додай сюди повільність (треба підняти півсистеми й дочекатися асинхронних ефектів) і незрозумілість («де саме розірвалось?») — і виходить дорогий генератор шуму замість сигналу.
Звідки береться формула 0.99^N і що вона показує?
Це проста оцінка спільної доступності ланцюга сервісів. Якщо сценарій проходить через N сервісів і кожен окремо доступний 99% часу, то ймовірність, що в момент прогону здорові всі одразу, приблизно дорівнює 0.99 в степені N. Для восьми сервісів це вже близько 92% — тобто приблизно кожен тринадцятий прогін червоний навіть за повністю справного продукту. Практичний висновок: чим довший наскрізний ланцюг, тим швидше падає надійність тесту, і великий e2e-набір математично приречений блимати. Саме тому стратегія не викорінює e2e, а стоншує його до кількох найкритичніших шляхів.
Що таке «сервіс в ізоляції» і що перевіряє компонентний тест?
Це підхід, коли ти піднімаєш один сервіс цілком — від його API до його бази — а всіх сусідів підміняєш дублерами (стабами чи mock-серверами на кшталт WireMock або msw). Такий тест часто називають компонентним. Він перевіряє поведінку сервісу як єдиного цілого: його логіку, роботу з даними й, що особливо цінно, обробку помилок і таймаутів сусідів. Оскільки відповіді сусідів задаєш ти сам, легко відтворити ситуації, яких на живому стенді не спровокуєш — падіння оплати, повільну відповідь складу, обірваний зв'язок. Плюси: швидко, детерміновано, повністю під контролем однієї команди.
У чому головна слабкість тестування сервісу в ізоляції?
Дублер сусіда — це твоє уявлення про сусіда, а не сам сусід. Ти запрограмував стаб віддавати певний формат, і тест зелений рівно доти, доки цей формат збігається з реальним. Щойно сусідній сервіс змінить структуру відповіді, код помилки чи додасть обов'язкове поле — твій стаб про це не дізнається, компонентний тест лишиться зеленим, а в проді взаємодія зламається. Тобто ізоляція чудово перевіряє логіку самого сервісу, але принципово не може довести, що сервіси реально домовляються між собою. Цю сліпу зону не закриє жоден ізольований тест — потрібен окремий механізм звірки з реальним сусідом.
Що таке інтеграційне середовище і чим воно платить за реалістичність?
Інтеграційне середовище — це коли справжні сервіси підняті й з'єднані між собою: реальні формати відповідей, справжня мережа, справжня асинхронність через черги. Його сила очевидна: воно ловить саме ті розбіжності на стиках, які ізоляція пропускає, бо тут ніхто нічого не імітує. Але платити доводиться всім тим самим, за що болить e2e: тести повільні, флакі, потрібен спільний стенд, який хтось постійно ламає викочуванням своєї версії, а підготовка даних вимагає засідити одразу кілька баз узгоджено. Ще одна прихована ціна — розмите володіння: коли стенд спільний, незрозуміло, хто відповідає за його цілісність, тому такі перевірки тримають обмежено, лише там, де без справжньої інтеграції не обійтися.
Що таке контрактний тест і яку прогалину він закриває?
Контрактний тест перевіряє, що двоє сервісів згодні щодо формату взаємодії, не піднімаючи обидва одночасно. Між швидкою-але-сліпою ізоляцією та реалістичним-але-дорогим інтеграційним стендом зяє прогалина: ізоляція не бачить розбіжностей на стику, а інтеграція надто важка, щоб покривати нею кожну пару сервісів. Контракт закриває саме її. Він фіксує спільну очікуваність — які поля, типи й статуси одна сторона обіцяє іншій — і перевіряє її дешево та детерміновано, без спільного середовища. Так ти отримуєш гарантію сумісності API, не платячи ціною інтеграційного стенду.
Як працюють consumer-driven contracts на прикладі Pact?
У підході consumer-driven contracts очікування диктує споживач (consumer) — той, хто викликає чужий сервіс. Його тест піднімає мок постачальника (provider), проганяє свій код проти цього моку і водночас записує у файл-контракт усе, що він від постачальника очікував: маршрути, поля, формати, статуси. Потім цей контракт потрапляє до постачальника (наприклад, через Pact Broker), і вже у своєму пайплайні постачальник програє записані очікування проти себе справжнього. Обидва тести залишаються швидкими та ізольованими, але разом дають те, чого не дає жодна ізоляція окремо: доказ, що мок споживача не розійшовся з реальним постачальником. Якщо постачальник ламає формат — його ж збірка червоніє на чужому контракті.
Чим контрактний тест відрізняється від інтеграційного?
Плутати їх — типова помилка. Інтеграційний тест піднімає обидва сервіси разом і перевіряє їхню взаємодію наживо, з усіма затратами спільного середовища. Контрактний тест не потребує двох сервісів одночасно: кожна сторона перевіряється окремо проти зафіксованого очікування — споживач проти моку, постачальник проти записаного контракту. Тобто контракт розв'язує ту саму задачу «чи збігаються формати», але без спільного стенду, швидко й детерміновано. Наслідок плутанини болючий: якщо команда вважає контракт різновидом інтеграційного тесту, вона або дублює роботу, або взагалі не робить контрактів і покладається на крихкий інтеграційний стенд там, де вистачило б дешевої звірки.
Чого контрактний тест принципово не перевіряє?
Контракт перевіряє форму взаємодії, а не її бізнес-результат. Він підтвердить, що сервіс оплати досі повертає поле status потрібного типу, але нічого не скаже про те, чи справді конкретне замовлення оплатилося й чи зарезервував його склад. Тобто контракт гарантує сумісність API двох сторін, а не коректність наскрізного сценарію через увесь ланцюг. Саме тому контракти не витісняють тонкий шар e2e повністю — вони прибирають потребу ганяти повний ланцюг лише заради перевірки «чи домовляються сервіси», але перевірку «чи проходить бізнес-сценарій наживо» лишають наскрізному тесту.
Що таке distributed tracing і навіщо воно потрібне тестувальнику?
Розподілене трасування зшиває один запит через усі сервіси в єдину картину, щоб було видно повний шлях: через які сервіси він пройшов, скільки часу провів у кожному й де саме зламався. Коли інтеграційний чи наскрізний тест падає, найдорожче питання — «у якому із сервісів розірвався ланцюг», і трасування відповідає на нього прямо, замість ручного зіставлення логів кількох команд. Для QA це не абстракція для SRE, а робочий інструмент: trace id з упалого тесту веде до сервіса-винуватця, той самий id у баг-репорті економить розробнику години, а вигляд трейсу показує, де запит застряг на черзі чи зовнішньому виклику — часте джерело плаваючих таймаутів. Практичне правило: якщо система інструментована трасуванням, вимагай до нього доступу.
Що таке trace id, span і заголовок traceparent?
На вході в систему запиту присвоюють унікальний ідентифікатор — trace id (його ще звуть correlation id). Далі цей ідентифікатор подорожує з кожним міжсервісним викликом через спеціальний заголовок: стандарт W3C Trace Context визначає для цього traceparent. Кожен сервіс на своєму боці записує відрізок власної роботи як span — проміжок із тим самим trace id, тривалістю й статусом. Коли зібрати всі span за одним trace id, складається повна траєкторія запиту через систему. Збирають цю телеметрію зазвичай через OpenTelemetry, а дивляться трейси в застосунках на кшталт Jaeger чи Zipkin.
Наскрізний тест «оформлення замовлення» впав. З чого почнеш діагностику?
Перше правило: у розподіленій системі червоний e2e найчастіше означає не баг продукту, а проблему середовища, тому спершу перевіряю саме її. Крок один — чи всі задіяні сервіси взагалі підняті й чи вони в сумісних версіях: на спільному стенді легко впіймати розсинхрон, коли оплата вже на новій версії, а склад ще на старій. Крок два — беру trace id упалого прогону й за трасуванням дивлюся, у якому сервісі й на якому span розірвався ланцюг, замість того щоб наосліп копати логи всіх команд. Крок три — дивлюся, чи це не асинхронний ефект: повідомлення в черзі могло ще не доїхати, і тест просто перевірив результат зарано. І лише коли середовище, версії й асинхронність відкинуто, роблю висновок про баг — з конкретним сервісом і span як доказом, а не «здається».
Як виглядає стратегія покриття мікросервісної системи й чому вона має форму «стільника»?
Принцип той самий, що й у піраміди тестування — штовхати перевірку якнайнижче, — але для мікросервісів форма змінюється на «стільник» (honeycomb): вузький верх наскрізних тестів, широкий середній шар компонентних, контрактних та обмежених інтеграційних перевірок і вузький низ юніт-тестів. Юніт-тести на рівні системи стають деталлю реалізації окремого сервіса й лишаються відповідальністю його команди. Основну масу впевненості дають компонентні тести (сервіс в ізоляції з дублерами) і контрактні (сумісність кожної пари споживач-постачальник). Інтеграційні тримають лише там, де контракту замало — складні асинхронні потоки через черги. А наскрізних лишають кілька штук на найкритичніші бізнес-шляхи як смоук довіри до системи в цілому.
Яке правило пріоритету при виборі рівня, на якому писати тест?
Перед тим як писати наскрізний тест, спитай себе: чи не ловиться цей ризик дешевше нижче? Розбіжність у форматі відповіді сусіда ловиться контрактним тестом — не потрібен весь ланцюг. Логіка обробки помилки чи таймауту сусіда ловиться компонентним тестом із дублером, який ти сам змусиш віддати цю помилку. Складна асинхронна взаємодія через чергу, якої контракт не покриває, — обмежений інтеграційний тест. І лише питання «чи справді весь бізнес-сценарій проходить наживо через реальні сервіси» виправдовує тонкий e2e. Тобто наскрізний тест — це остання інстанція, а не перший рефлекс: кожен рівень нижче дешевший, швидший і точніше вказує на винуватця.
Чому «зелено в ізоляції» не дорівнює «працює в проді»?
Тому що компонентний тест перевіряє сервіс проти твого уявлення про сусідів, а не проти самих сусідів. Стаб віддає рівно те, що ти в нього заклав, і нічого не знає про новий обов'язковий заголовок, змінений код помилки чи новий формат дати в реального сервіса. Поки стаб і реальність збігаються, все зелене; щойно сусід тихо змінив контракт, а твій стаб — ні, тест лишається зеленим, а прод ловить розсинхрон. Тобто зелений ізольований тест доводить лише внутрішню коректність сервіса за твоїх припущень про оточення. Щоб «зелено в ізоляції» справді означало «сумісно з реальністю», ці припущення має перевіряти контракт — інакше стаб непомітно застаріває.
Чому не можна просто покрити все наскрізними тестами — це ж найреалістичніше?
Реалістичність тут ілюзорна, а ціна максимальна. Спільна доступність довгого ланцюга падає за 0.99^N, тому великий наскрізний набір математично приречений блимати червоним не через баги, а через моргання середовища — і поступово команда перестає йому вірити. Додай флак асинхронності, який кожен e2e успадковує від усіх ланок разом, повільність (підняти півсистеми — це хвилини на тест), розсинхрон версій на спільному стенді й муку локалізації винного сервіса. Виходить найдорожчий і найкрихкіший спосіб перевірити те, що дешево ловиться нижче. Сильна відповідь одразу пропонує альтернативу: стоншити e2e до кількох критичних шляхів, а решту впевненості добути контрактами й компонентними тестами.
Як розподілена архітектура ускладнює ізоляцію тест-даних?
У моноліті стан лежить в одній базі, тож підготувати й прибрати дані тесту відносно просто. У мікросервісах стан розмазаний по кількох базах, які належать різним сервісам, тому засідити узгоджений сценарій означає покласти пов'язані дані одразу в кілька сховищ і врахувати, що між ними панує кінцева узгодженість (eventual consistency) — записане в одному сервісі з'явиться в іншому не миттєво. Через це ізоляція прогонів стає критичною вдвічі: без окремого tenant чи унікальних даних на кожен прогін тести починають чіплятися за чужий стан і флакати. Плюс прибирання складніше — teardown має пройти по всіх задіяних сервісах, інакше залишки в одній базі отруять наступний прогін.
Три кейси, що переводять стратегію в дію: таблиця вибору рівня для конкретних ризиків, діагностика червоного наскрізного тесту через trace id і демонстрація прогалини «стаб зелений, а прод зламано», яку закриває контракт. Скрізь — не «як влаштовано взагалі», а що вирішувати руками QA.
Кейс 1. На якому рівні ловити ризик: таблиця рішень
Прийшла задача покрити оформлення замовлення. Спокуса — написати один великий наскрізний тест на весь сценарій. Дисципліна — спершу розкласти ризики й для кожного знайти найдешевший рівень, де він ловиться. Ось як виглядає таке рознесення для типового ланцюга «кошик → оплата → склад → нотифікації».
| Ризик, який треба покрити | Найдешевший рівень | Чому не наскрізний |
|---|---|---|
Сервіс оплати змінив поле status у відповіді | Контрактний | Розбіжність формату видно на парі споживач-постачальник без підняття ланцюга |
Сервіс складу відповів 500 — кошик має показати «спробуйте пізніше» | Компонентний (дублер складу віддає 500) | Живий склад так просто не зламаєш; стаб відтворює помилку детерміновано |
| Оплата тупить 3 секунди — кошик не має зависати | Компонентний (дублер із затримкою) | Реальний таймаут на стенді не спровокуєш і не повториш |
| Розрахунок знижки всередині кошика | Юніт | Чиста логіка, мережа ні до чого |
| Подія «оплачено» реально доходить до складу через чергу | Інтеграційний (обмежено) | Контракт перевіряє формат події, але не факт доставки через брокер |
| Клієнт справді може оформити замовлення від початку до кінця | Тонкий e2e (1 шлях) | Це смоук довіри до системи, а не спосіб покрити кожну гілку |
Що дивитися і чому:
- Більшість рядків пішли вниз від e2e. З шести ризиків наскрізний тест виправданий рівно для одного — «весь бізнес-сценарій наживо». Решта ловиться дешевше, швидше й точніше вказує на винуватця.
- Обробка помилок сусіда — це компонентний рівень, не e2e. Щоб перевірити реакцію кошика на
500чи таймаут оплати, реальна оплата не потрібна й навіть шкідлива: живий сервіс не даси зламати за розкладом, а дублер віддасть будь-яку помилку детерміновано. - Формат vs доставка — різні рівні. «Оплата повертає правильне поле» — це контракт. «Подія справді доїхала до складу через чергу» — це вже асинхронна інтеграція, яку контракт не покриває. Плутати їх — значить або ганяти зайвий e2e, або лишити дірку в покритті.
Кейс 2. Червоний e2e: локалізуємо винний сервіс за trace id
Наскрізний тест «оформлення замовлення» впав на асерті фінального статусу. Перш ніж заводити баг на продукт, проходимо систематику: середовище → версії → трасування. Ключ до третього кроку — витягнути trace id відповіді й прикріпити його до звіту, щоб не копати логи чотирьох команд наосліп.
import { test, expect } from '@playwright/test';
test('оформлення замовлення проходить наскрізь', async ({ request }, testInfo) => {
const res = await request.post('https://shop.example.com/api/orders', {
data: { cartId: 'c-42', payment: 'card' },
});
// trace id прилітає у відповіді (W3C Trace Context) — чіпляємо його до звіту ДО асерту
const traceparent = res.headers()['traceparent'];
const traceId = traceparent?.split('-')[1]; // формат: version-traceid-spanid-flags
await testInfo.attach('trace-id', { body: traceId ?? 'відсутній', contentType: 'text/plain' });
expect(res.status(), `order failed, trace: ${traceId}`).toBe(201);
});
Коли асерт червоніє, у звіті вже лежить trace id — вставляємо його в Jaeger і бачимо повний шлях запиту зі span кожного сервіса:
trace 7b3f9a... оформлення замовлення 850 ms
├─ api-gateway POST /api/orders 12 ms ok
├─ cart-service reserve cart 40 ms ok
├─ payment-service charge 780 ms ERROR 504
└─ warehouse-service (не викликаний) —
Що дивитися і чому:
- Trace id перетворює «ланцюг розірвався» на «розірвався тут». Червоний e2e сам по собі каже лише, що результат не той. Span із
ERROR 504наpayment-serviceпрямо називає ланку й навіть характер — таймаут шлюзу, а не логічну відмову. 504на span — привід перевірити середовище, а не одразу писати баг. Gateway timeout часто означає, що апстрім живий, але повільний: важкий запит до БД, прогрів після деплою, навантаження на стенді. Спершу дивимося, чи оплата взагалі в робочому стані й у сумісній версії.- Trace id у баг-репорті економить години. Розробник оплати відкриває той самий трейс і бачить повний контекст запиту без переписки «а що ти надсилав і коли». Атач trace id до звіту робимо завжди, а не лише коли впало.
Кейс 3. Прогалина «стаб зелений — прод зламано» і як її закриває контракт
Кошик тестуємо в ізоляції: сусідній сервіс оплати підмінено моком (msw), який віддає заздалегідь відому відповідь. Тест перевіряє, що кошик правильно читає поле status.
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';
// наше УЯВЛЕННЯ про оплату: віддає status рядком
const server = setupServer(
http.post('https://payments/charge', () =>
HttpResponse.json({ status: 'paid', amount: 100 }),
),
);
Компонентний тест кошика проти цього моку зелений. А тепер оплата в новому релізі перейшла на enum і повертає status: 'PAID' великими літерами. Мок про це не знає — він досі віддає 'paid', тест лишається зеленим, а в проді кошик не впізнає статус і замовлення зависає. Ізоляція цю розбіжність не бачить за побудовою. Ловить її контракт: тест-споживач записує очікування, і збірка постачальника перевіряє його проти себе справжнього.
// consumer (кошик) фіксує, що він реально очікує від оплати
await provider
.given('платіж успішний')
.uponReceiving('запит на списання')
.withRequest({ method: 'POST', path: '/charge' })
.willRespondWith({
status: 200,
body: { status: like('paid'), amount: like(100) }, // ← зафіксований формат
});
// цей контракт публікується в Pact Broker; збірка payment-service програє його проти себе
Що дивитися і чому:
- Зелений стаб не доводить сумісності. Мок повертає рівно те, що ти в нього заклав, тож він завжди «згоден» сам із собою. Розбіжність
'paid'проти'PAID'— саме той клас багів, який ізоляція пропускає, а прод ловить. - Контракт ловить дрейф формату без спільного стенду. Коли оплата зламає формат, червоніє її власна збірка на кроці верифікації контракту — до деплою, а не в проді. При цьому обидва тести лишаються швидкими й ізольованими: ланцюг піднімати не треба.
- Контракт не заміняє e2e повністю. Він підтверджує «оплата віддає поле
statusправильної форми», але не «конкретне замовлення оплатилося й склад його зарезервував». Тому пара «контракт + тонкий e2e» покриває і форму взаємодії, і живий бізнес-результат — кожен своїм дешевим шаром.
Моноліт vs мікросервіси очима QA
- Можу пояснити, що в моноліті виклики між компонентами внутрішньопроцесні (як виклик функції), а в мікросервісах — мережеві, з таймаутами й частковими відмовами.
- Знаю, що ризик зсувається з логіки всередині сервіса на стики між сервісами, і саме туди має дивитися стратегія.
- Розумію, чому в розподіленій системі немає одного повного й стабільного середовища, на якому можна прогнати «всю систему».
Чому e2e стоншують, а не викорінюють
- Можу вивести формулу
0.99^Nі пояснити, чому довгий наскрізний ланцюг блимає червоним навіть без багів. - Можу пояснити проблему розсинхрону версій (version skew) на спільному стенді, якого в проді не буває.
- Розумію, що e2e лишають тонким шаром для кількох критичних бізнес-шляхів, а не як спосіб покрити всі гілки.
Сервіс в ізоляції vs інтеграційне середовище
- Знаю різницю між компонентним тестом (один сервіс + дублери) та інтеграційним (реальні сервіси разом).
- Можу пояснити, чому ізоляція швидка й детермінована, але не бачить розбіжностей стаба з реальним сусідом.
- Розумію, що інтеграційне середовище ловить справжні розбіжності на стиках, але платить флаком, повільністю й розмитим володінням спільним стендом.
Контрактні тести
- Можу пояснити, яку прогалину між ізоляцією та інтеграцією закриває контракт.
- Знаю, як працюють consumer-driven contracts: споживач записує очікування, постачальник програє їх проти себе (Pact, Pact Broker).
- Чітко відділяю контракт від інтеграційного тесту: контракт не потребує обох сервісів одночасно.
- Розумію межу контракту: він перевіряє форму взаємодії (поля, типи, статуси), а не бізнес-результат наскрізного сценарію.
Distributed tracing для QA
- Розумію ланцюг trace id → span → повна траєкторія запиту через сервіси.
- Знаю, що trace id передається між сервісами через заголовок
traceparent(стандарт W3C Trace Context). - Розумію три користі для QA: локалізація винного сервіса, атач trace id до баг-репорту, розуміння, де запит застряг на асинхронності.
Стратегія покриття за рівнями
- Можу намалювати «стільник» (honeycomb): тонкий e2e зверху, широкий шар компонентних і контрактних усередині, вузький юніт унизу.
- Знаю розкладку рівнів: юніт → компонентні → контрактні → інтеграційні → тонкий e2e, і що на якому ловиться.
- Володію правилом пріоритету: перед наскрізним тестом питаю, чи не ловиться ризик дешевше нижче.
- Розумію, чому ізоляція тест-даних у розподіленій системі критична вдвічі — стан розмазаний по кількох базах.
Куди зсувається головний ризик при переході від моноліту до мікросервісів?
Питання
Моноліт vs мікросервіси: як змінюється виклик між компонентами?