QA Interview Hub · База знань · Тести

System Design

Мікросервіси і стратегія тестування

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

У моноліті e2e-тест покриває все одразу. У мікросервісах 30 сервісів, кожна команда деплоїть незалежно, і повний e2e на кожну зміну стає повільним і нестабільним. Тому стратегія інша.

              e2e (мало, критичні шляхи)
         contract tests (між сервісами)
    component tests (сервіс ізольовано, залежності - заглушки)
  integration (сервіс + його БД, черга)
unit (багато)

Component test - тестуємо один сервіс через його API, з реальною БД у контейнері (Testcontainers), а сусідні сервіси замінені заглушками. Швидко, стабільно, запускається в CI сервісу.

Contract test - перевіряє, що producer і consumer однаково розуміють API. Consumer-driven підхід (Pact): consumer описує, які запити він робить і яких відповідей чекає, а producer у своєму CI перевіряє, що виконує ці контракти. Так ламаюча зміна API ловиться до деплою, без спільного середовища.

E2E залишаємо для кількох критичних бізнес-потоків: реєстрація, поповнення, переказ, виведення коштів. Їх не має бути сотні.

Що ще важливо:

  • Тестування асинхронних контрактів: формат подій у черзі теж контракт.
  • Зворотна сумісність: новий сервіс має приймати старий формат, поки всі consumer не оновилися.
  • Кожна команда володіє тестами свого сервісу. QA-архітектор задає стандарти і спільні інструменти.
На співбесіді: Для мікросервісів я зміщую основну вагу на component і contract тести, які йдуть у CI кожного сервісу без спільного середовища. E2E залишаю тільки для кількох критичних грошових потоків, бо сотні e2e через 30 сервісів будуть повільними і нестабільними.

Задача: як тестувати систему переказів з чергою

Умова: користувач створює переказ через API. Transfer Service записує переказ зі статусом PENDING і публікує подію в чергу. Ledger Service списує і зараховує кошти, потім публікує подію, і статус стає COMPLETED. Notification Service надсилає push.

Client -> Transfer API -> DB (PENDING) -> Queue -> Ledger -> Queue -> Transfer (COMPLETED)
                                                       \-> Notification

Вимоги і ризики

Гроші не мають списатися двічі або загубитися. Баланс не йде в мінус. Кожен переказ доходить до кінцевого статусу: COMPLETED або FAILED. Повторний запит клієнта не створює другий переказ.

Шари тестів

  1. Unit: розрахунок комісії, валідація суми, перехід статусів.
  2. Component Transfer Service: API плюс реальна БД, черга - заглушка або контейнер. Перевіряємо, що подія опублікована з правильним форматом.
  3. Component Ledger: подаємо подію в чергу, перевіряємо записи в ledger. Дубль події - одне списання.
  4. Contract: формат події між Transfer і Ledger.
  5. E2E: створити переказ через API, дочекатися COMPLETED через expect.poll, перевірити обидва баланси.

Негативні сценарії

  • Недостатньо коштів - статус FAILED, баланс не змінився.
  • Повтор POST з тим самим idempotency key - той самий переказ.
  • Ledger впав посеред обробки - після рестарту переказ завершується один раз.
  • Невалідна подія - потрапляє в DLQ, є алерт.
  • 20 паралельних переказів з одного рахунку на суму більшу за баланс - жоден не дає мінус.

Метрики

Кількість переказів, що "зависли" в PENDING довше N хвилин, розміри DLQ, consumer lag, розбіжності при звірці балансів.

На співбесіді: Я розбиваю систему на шари: unit для логіки, component для кожного сервісу з реальною БД, contract для формату подій і кілька e2e з очікуванням кінцевого статусу. Основний фокус - негативні сценарії: дублікати, падіння consumer, паралельні списання і завислі в PENDING перекази.

Eventual consistency і саги в тестах

Eventual consistency означає, що після зміни дані в різних частинах системи стають узгодженими не одразу, а через деякий час. Баланс у Ledger уже змінився, а в сервісі історії ще ні.

Як тестувати:

  • Не перевіряти одразу після дії. Чекати кінцевого стану через polling з таймаутом: expect.poll або expect(...).toPass().
  • Таймаут має відповідати SLA. Якщо бізнес обіцяє оновлення за 5 секунд, тест з таймаутом 60 секунд пропустить деградацію.
  • Ніяких фіксованих sleep: вони або занадто короткі (flaky), або занадто довгі (повільно).

Saga - спосіб виконати бізнес-операцію через кілька сервісів без розподіленої транзакції. Кожен крок має компенсуючу дію. Наприклад, замовлення в e-commerce: зарезервувати товар, списати оплату, створити доставку. Якщо доставку створити не вдалося - повернути гроші і зняти резерв.

Що тестувати в сазі:

  • Падіння на кожному кроці і правильну компенсацію.
  • Падіння самої компенсації: чи є повтор і алерт.
  • Повторну доставку подій на кожному кроці.
  • Кінцевий стан: немає "напівзавершених" операцій, де гроші списані, а товар не зарезервований.
await expect(async () => {
  const order = await api.getOrder(orderId);
  expect(order.status).toBe('CANCELLED');
  expect(order.refundStatus).toBe('REFUNDED');
}).toPass({ timeout: 10_000 });
На співбесіді: В асинхронній системі я перевіряю кінцевий стан через polling з таймаутом, який відповідає SLA, а не через sleep. Для саг я тестую падіння на кожному кроці і що компенсація повертає систему в узгоджений стан, наприклад гроші повертаються, якщо замовлення скасовано.