Тестування мікросервісів: стратегія
Зміст
Ти прийшов на проєкт, де замість одного застосунку — сорок сервісів: окремі репозиторії, окремі бази даних, окремі команди, кожна свій шматок по десять разів на день. Ти пишеш звичний «користувач оформлює замовлення», і він падає щоранку — то один сервіс лежить, то другий викотив несумісну версію, то повідомлення в черзі ще не доїхало. Питання «як це взагалі тестувати, щоб не з'їхати з глузду» — і є темою цієї глави.
Це глава-поглиблення: якщо ти вперше проходиш розділ про автоматизацію, її можна пропустити й повернутися пізніше — вона спирається на , й контрактні тести, які краще спершу зрозуміти окремо. Але для senior-рівня це обов'язкова картина світу: стратегію розподіленої системи запитують саме тоді, коли перевіряють, чи ти мислиш архітектурою, а не окремим тестом. Тут ми не переказуємо механіку контрактних тестів чи (для них є свої глави) — ми збираємо їх в одну стратегію: що, на якому рівні й чому.
Моноліт vs мікросервіси очима QA
Моноліт (monolith) — це один застосунок, який деплоять цілком. Виклики між його частинами відбуваються всередині одного процесу — це звичайні виклики функцій: миттєві, надійні, або впали разом з усім процесом. Дані зазвичай лежать в одній базі, тож цілісність гарантує . Для QA це зручно: є одне середовище, яке цілком реальне, один деплой, одна версія. Наскрізний тест через UI чи API перевіряє систему такою, якою вона поїде в прод.
Мікросервіси (microservices) розбивають цю монолітну брилу на багато незалежних сервісів. Кожен має власну кодову базу, власну базу даних і власний цикл релізу. Спілкуються вони вже через мережу — синхронно (HTTP/gRPC) або асинхронно (черги повідомлень, події). Головна зміна для тестувальника: поведінка системи більше не «живе» в одному місці — вона виникає з взаємодій. Замовлення проходить через сервіс кошика, сервіс оплати, сервіс складу, сервіс нотифікацій, і кожен стик між ними — це мережа, з усіма її радощами: , , часткові , .
| Аспект | Моноліт | Мікросервіси |
|---|---|---|
| Виклик між компонентами | Виклик функції в процесі | Мережевий запит |
| Дані | Одна база, транзакції | База на сервіс, eventual consistency |
| Деплой | Один, цілком | Незалежний на кожен сервіс |
| «Реальне» середовище | Одне, повне | Немає одного повного й стабільного |
| Де живе баг інтеграції | Всередині коду | На стиках, у мережі |
| Локалізація падіння | Один стек-трейс | Треба знайти винний сервіс |
Ключовий висновок: у моноліті найдорожчий — логіка всередині; у мікросервісах він зміщується на межі між сервісами. Саме туди має дивитися стратегія тестування.
Чому e2e в мікросервісній архітектурі болить
Наскрізний (end-to-end, e2e) тест проганяє реальний користувацький сценарій через усі сервіси одразу. У моноліті це нормальний основний інструмент. У мікросервісах — джерело постійного болю, і ось чому.
Крихкість множиться. Якщо сценарій зачіпає вісім сервісів і кожен окремо доступний у 99% часу, спільна доступність усього ланцюга — це вже приблизно 0.99^8, тобто близько 92%. Приблизно кожен тринадцятий прогін червоний не через баг, а тому що якийсь один сервіс саме перевантажений, редеплоїться чи моргнув мережею. Тест перетворюється на генератор шуму.
накопичується. Кожен мережевий стик додає асинхронність, таймаути й eventual consistency (). Це не абстракція з підручника, а режим за замовчуванням у реальних сховищах: дока DynamoDB описує його так — «the responses may not reflect the results of a recently completed write operation. If you repeat your read request after a short time, the response should eventually return the more recent item». Практичний висновок для тесту звідти ж: сильна узгодженість буває опцією запиту, і перемкнути її часто дешевше, ніж накручувати . Один e2e-тест успадковує усіх ланок разом — детальніше причини й лікування розбирає глава Флакі-тести.
Незрозуміло, хто винен. Тест «оформлення замовлення» впав. У якому з восьми сервісів? Червоний e2e показує лише, що ланцюг розірвався, але не де саме. Без трейсингу діагностика перетворюється на розкопки по логах кількох команд.
Проблема середовища й версій. Щоб e2e був чесним, усі сервіси мають бути піднятими й у сумісних версіях одночасно. Але сервіс оплати вже викотив v5, а сервіс складу ще на v4 — на спільному стенді ти ловиш розсинхрон версій (version skew), якого в проді ніколи не буде. Термін галузевий, і означення в джерелі ширше за наше: це «a state of a distributed system in which it contains multiple incompatible versions of code, data, and/or configuration» — тобто розходитися можуть не лише версії сервісів, а й дані з конфігурацією. Наша теза там теж є, майже дослівно: стенд дає хибні позитиви — «problems that occur in a particular staged combination that wouldn't actually be spotted in production». Що джерело радить натомість: ловити розсинхрон у процесі просування release candidate середовищами, причому просувати разом із конфігурацією. Чиє це середовище, хто відповідає за його цілісність — окреме організаційне питання без простої відповіді.
Повільний фідбек. Підняти півсистеми, засідити дані в кількох базах, дочекатися асинхронних ефектів — це хвилини на тест. Наскрізний набір з сотень таких тестів не влазить у швидкий цикл 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, і його офіційна дока прямо каже, що це «a Cloud Native Computing Foundation (CNCF) project», який виник зі злиття OpenTracing і OpenCensus. Слова «стандарт-де-факто» ми свідомо не вживаємо: такого статусу дока собі не присвоює. Дивляться трейси у застосунках на кшталт Jaeger чи Zipkin. Механіку роботи з логами й трейсингом під час діагностики докладніше розбирає окрема глава про у розділі про Git і CI/CD; performance-кут (APM, розподілений трейс під навантаженням) — у розділі про performance-тестування. Тобі як QA достатньо знати: якщо система інструментована трейсингом, вимагай доступу до нього — це найшвидший шлях від червоного тесту до винного сервісу.
Стратегія покриття системи за рівнями
Тепер зберемо все в одну стратегію. Принцип той самий, що й у піраміди тестування — штовхай перевірку якнайнижче, — але для мікросервісів центр ваги зсувається до середини: вузький верх e2e, широкий середній шар інтеграційних і компонентних тестів, вузький низ (їх пише команда сервісу, і на рівні системи вони вже деталь реалізації). Цю форму інколи називають Testing Honeycomb, проте розгорнутого опису самої моделі в наших джерелах немає, тож спирайся на рівні, а не на назву.
Як розкласти покриття по рівнях:
- Модульні — логіка всередині сервісу; відповідальність команди сервісу.
- Компонентні — сервіс в ізоляції: його 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) і обґрунтовує, що на якому рівні ловиться.
Джерела
Моноліт vs мікросервіси очима QA
- Toby Clemson — Testing Strategies in a Microservice Architecture — у мікросервісній архітектурі компонент — це сам сервіс: «добре інкапсульована, звʼязна й незалежно замінна частина більшої системи» (
trust: secondary, dec-0311). - ISTQB Glossary — канонічні означення тих самих слів, які тут означують інші обʼєкти:
component— «A part of a system that can be tested in isolation»,integration testing— «A test level that focuses on interactions between components or systems».
Чому e2e в мікросервісній архітектурі болить
- Toby Clemson — Testing Strategies in a Microservice Architecture — що саме дає наскрізний рівень у мікросервісах і за що йому платять: проміжки між сервісами й — фаєрволи, , балансувальники (
trust: secondary, dec-0311). - Software Engineering at Google — Chapter 23: Continuous Integration — джерело поняття
version skew(«a state of a distributed system in which it contains multiple incompatible versions of code, data, and/or configuration») і класу хибних падінь спільного стенду — «problems that occur in a particular staged combination».
Сервіс в ізоляції vs інтеграційне середовище
- Toby Clemson — Testing Strategies in a Microservice Architecture — вигода ізоляції названа конкретно й не зводиться до швидкості: відтворюване відтворення помилкових сценаріїв; там же перелічені розвилки реалізації компонентного тесту — у процесі чи через мережу, дублери всередині сервісу чи назовні, справжнє сховище чи in-memory (
trust: secondary, dec-0311). - Software Engineering at Google — Chapter 23: Continuous Integration — що джерело протиставляє спільному стенду: просування release candidate середовищами разом із конфігурацією, а не єдине «інтеграційне» оточення.
- Toby Clemson — Testing Strategies in a Microservice Architecture — контрактний тест живе на межі зовнішнього сервісу, кожен споживач формує свій контракт, і контрактні тести не є компонентними — поведінку глибоко вони не перевіряють; при цьому контракт включає не лише структури даних, а й побічні ефекти та характеристики продуктивності (
trust: secondary, dec-0311). - Pact — docs — інструментальна реалізація consumer-driven контрактів: очікування споживача фіксуються у виконуваному контракті, який верифікує постачальник.
- ISTQB Glossary — канонічне означення:
contract testing— «A type of integration testing that verifies that the interfaces are used as specified in their contracts».
Distributed tracing (оглядово)
- OpenTelemetry — Traces — механіка: трейс складається зі спанів зі спільним
trace_id, вкладених черезparent span ID, а розподіленим трейс робить поширення контексту. - OpenTelemetry — What is OpenTelemetry? — джерело обох фактів про проєкт: розкривається як Cloud Native Computing Foundation, а сам проєкт виник зі злиття OpenTracing і OpenCensus.
- W3C — Trace Context — специфікація заголовка
traceparent, яким контекст і передається між сервісами. - ISTQB® CTAL-TAE Syllabus v2.0 — бік автоматизації: унікальний ID взаємодії (correlation ID / trace ID) може логуватися рішенням автоматизації, щоб допомогти аналізувати результати.
Стратегія покриття системи за рівнями
- Toby Clemson — Testing Strategies in a Microservice Architecture — джерело самої розкладки рівнів (модульний, компонентний, інтеграційний, контрактний, наскрізний) і того, що розмір «юніта» строго не визначений, а соціальний і самотній стилі — не конкуренти (
trust: secondary, dec-0311). - ISTQB Glossary — означення
test pyramidяк графічної моделі співвідношення обсягу тестування за рівнями, з якою цю розкладку звіряють. - AWS — DynamoDB read consistency — чому «ще не доїхало» це режим, а не баг: кінцева узгодженість є режимом читання за замовчуванням, а сильна вмикається опцією запиту.
- Software Engineering at Google — Chapter 23: Continuous Integration — той самий діагноз у формулюванні джерела: false positives спільного стенду, і дельта, про яку зазвичай не думають, — сам створює розсинхрон, бо кілька версій живуть одночасно.
- OpenTelemetry — Traces — інструмент першого кроку діагностики: спани зі спільним
trace_idпоказують, де саме в ланцюгу зупинився запит.
Чим тестування мікросервісів відрізняється від моноліту з погляду 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. Кожен сервіс на своєму боці записує відрізок власної роботи як із тим самим trace id, тривалістю й статусом. Коли зібрати всі span за одним trace id, складається повна траєкторія запиту через систему. Збирають цю телеметрію зазвичай через OpenTelemetry, а дивляться трейси в застосунках на кшталт Jaeger чи Zipkin.
Наскрізний тест «оформлення замовлення» впав. З чого почнеш діагностику?
Перше правило: у розподіленій системі червоний e2e найчастіше означає не баг продукту, а проблему середовища, тому спершу перевіряю саме її. Крок один — чи всі задіяні сервіси взагалі підняті й чи вони в сумісних версіях: на спільному стенді легко впіймати розсинхрон, коли оплата вже на новій версії, а склад ще на старій. Крок два — беру trace id упалого прогону й за трейсом дивлюся, у якому сервісі й на якому span розірвався ланцюг, замість того щоб наосліп копати логи всіх команд. Крок три — дивлюся, чи це не асинхронний ефект: повідомлення в черзі могло ще не доїхати, і тест просто перевірив результат зарано. І лише коли середовище, версії й асинхронність відкинуто, роблю висновок про баг — з конкретним сервісом і span як доказом, а не «здається».
Як виглядає стратегія покриття мікросервісної системи?
Принцип той самий, що й у — штовхати перевірку якнайнижче, — але для мікросервісів центр ваги зсувається до середини: вузький верх наскрізних тестів, широкий середній шар компонентних, контрактних та обмежених інтеграційних перевірок і вузький низ . Цю форму інколи називають Testing Honeycomb, але опису самої моделі в наших джерелах немає, тож називати її так на співбесіді варто обережно. Модульні тести на рівні системи стають деталлю реалізації окремого сервіса й лишаються відповідальністю його команди. Основну масу впевненості дають компонентні тести (сервіс в ізоляції з дублерами) і контрактні (сумісність кожної пари споживач-постачальник). Інтеграційні тримають лише там, де контракту замало — складні асинхронні потоки через черги. А наскрізних лишають кілька штук на найкритичніші бізнес-шляхи як довіри до системи в цілому.
Яке правило пріоритету при виборі рівня, на якому писати тест?
Перед тим як писати наскрізний тест, спитай себе: чи не ловиться цей ризик дешевше нижче? Розбіжність у форматі відповіді сусіда ловиться контрактним тестом — не потрібен весь ланцюг. Логіка обробки помилки чи таймауту сусіда ловиться компонентним тестом із дублером, який ти сам змусиш віддати цю помилку. Складна асинхронна взаємодія через чергу, якої контракт не покриває, — обмежений інтеграційний тест. І лише питання «чи справді весь бізнес-сценарій проходить наживо через реальні сервіси» виправдовує тонкий e2e. Тобто наскрізний тест — це остання інстанція, а не перший рефлекс: кожен рівень нижче дешевший, швидший і точніше вказує на винуватця.
Чому «зелено в ізоляції» не дорівнює «працює в проді»?
Тому що компонентний тест перевіряє сервіс проти твого уявлення про сусідів, а не проти самих сусідів. Стаб віддає рівно те, що ти в нього заклав, і нічого не знає про новий обов'язковий заголовок, змінений код помилки чи новий формат дати в реального сервіса. Поки стаб і реальність збігаються, все зелене; щойно сусід тихо змінив контракт, а твій стаб — ні, тест лишається зеленим, а прод ловить розсинхрон. Тобто зелений ізольований тест доводить лише внутрішню коректність сервіса за твоїх припущень про оточення. Щоб «зелено в ізоляції» справді означало «сумісно з реальністю», ці припущення має перевіряти контракт — інакше стаб непомітно застаріває.
Чому не можна просто покрити все наскрізними тестами — це ж найреалістичніше?
Реалістичність тут ілюзорна, а ціна максимальна. Спільна доступність довгого ланцюга падає за 0.99^N, тому великий наскрізний набір математично приречений блимати червоним не через баги, а через моргання середовища — і поступово команда перестає йому вірити. Додай асинхронності, який кожен e2e успадковує від усіх ланок разом, повільність (підняти півсистеми — це хвилини на тест), на спільному стенді й муку локалізації винного сервіса. Виходить найдорожчий і найкрихкіший спосіб перевірити те, що дешево ловиться нижче. Сильна відповідь одразу пропонує альтернативу: стоншити e2e до кількох критичних шляхів, а решту впевненості добути контрактами й компонентними тестами.
Як розподілена архітектура ускладнює ізоляцію тест-даних?
У моноліті стан лежить в одній базі, тож підготувати й прибрати дані тесту відносно просто. У мікросервісах стан розмазаний по кількох базах, які належать різним сервісам, тому засідити узгоджений сценарій означає покласти пов'язані дані одразу в кілька сховищ і врахувати, що між ними панує (eventual consistency) — записане в одному сервісі з'явиться в іншому не миттєво. Це режим за замовчуванням у реальних сховищах, а не крайній випадок: дока DynamoDB прямо пише, що відповідь на читання «may not reflect the results of a recently completed write operation», і що повторний запит через короткий час поверне свіжіші дані. Через це ізоляція прогонів стає критичною вдвічі: без окремого 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 }),
),
);
Компонентний тест кошика проти цього моку зелений. А тепер оплата в новому релізі перейшла на і повертає 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 до баг-репорту, розуміння, де запит застряг на асинхронності.
Стратегія покриття за рівнями
- Можу намалювати розкладку рівнів: тонкий e2e зверху, широкий шар компонентних і контрактних усередині, вузький шар модульних унизу (назву
Testing Honeycombвживаю обережно — опису моделі в наших джерелах немає). - Знаю розкладку рівнів: модульні → компонентні → контрактні → інтеграційні → тонкий e2e, і що на якому ловиться.
- Володію правилом пріоритету: перед питаю, чи не ловиться ризик дешевше нижче.
- Розумію, чому ізоляція тест-даних у розподіленій системі критична вдвічі — стан розмазаний по кількох базах.
Квіз
Перед стартом
- Питань: 12
- Поріг «зараховано»: ≥70% правильних відповідей.
- Результат впливає на прогрес; завалені питання підуть у чергу повторення.
- Квіз впливає на компліт теми: тема стає «пройдено», лише коли прочитано теорію І квіз складено на ≥70%.
Питання
Моноліт vs мікросервіси: як змінюється виклик між компонентами?