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

    06 · Автоматизація: стратегія

    Тестування мікросервісів: стратегія

    Зміст

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

    Це глава-поглиблення: якщо ти вперше проходиш розділ про автоматизацію, її можна пропустити й повернутися пізніше — вона спирається на піраміду тестування, тестові дублери й контрактні тести, які краще спершу зрозуміти окремо. Але для 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-тест: оформити замовлення

    API Gateway

    Сервіс кошика

    Сервіс оплати

    Сервіс складу

    Черга подій

    Сервіс нотифікацій

    Пошта, стороннє API

    e2e-тест: оформити замовлення

    API Gateway

    Сервіс кошика

    Сервіс оплати

    Сервіс складу

    Черга подій

    Сервіс нотифікацій

    Пошта, стороннє API

    Кожна стрілка — мережевий стик, який може моргнути, протаймаутити чи віддати несумісну відповідь. Тест зелений, лише коли зелений увесь ланцюг. Звідси головна ідея стратегії: 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, широкий середній шар інтеграційних і компонентних тестів, вузький низ юніт-тестів (їх пише команда сервісу, і на рівні системи вони вже деталь реалізації).

    E2E: кілька критичних наскрізних шляхів
    дорого, крихко, повільно

    Інтеграційні: сервіс + реальні сусіди
    реальні стики, обмежено

    Контрактні: чи домовляються сервіси
    дешево, детерміновано

    Компонентні: сервіс в ізоляції з дублерами
    швидко, логіка + обробка помилок

    Юніт: логіка всередині сервісу
    найдешевше

    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).