Уявіть команду, що приймає цілком розумне рішення: "Давайте перевіряти нашу систему так, як її перевіряє реальний користувач — натискаємо кнопки, заповнюємо форми, перевіряємо результати". Логічно, правда? Selenium або Playwright, браузер, реальна база даних, реальний сервер.
Через рік у проєкті — 500 Selenium тестів. Повний запуск займає 4 години. Кожен третій тест час від часу провалюється "просто так" — браузер не відкрився, з'єднання з БД перервалось, елемент не знайшовся через мілісекунду затримки. Розробники перестають запускати тести локально — чекати годину незручно. CI/CD pipeline займає всю ніч. Feedback loop — 8 годин. Команда вже не боїться помилитися в деплої — вони вже бояться тестів.
Це Ice Cream Cone Anti-Pattern (анти-патерн морозива): верхня частина важка та дорога (E2E), нижня — легка й порожня (unit). Перевернута піраміда, де найповільніші і найненадійніші тести є основою стратегії.
Майк Кон у 2009 році описав піраміду тестування як антидот до цього анти-патерну. Але перш ніж занурюватись у деталі — розберемось у критеріях, за якими порівнюються рівні тестування.
Будь-який тест можна охарактеризувати по чотирьох ключових аксях.
⚡ Швидкість (Speed)
💰 Вартість (Cost)
🔒 Ізоляція (Isolation)
🔄 Зворотний зв'язок (Feedback)
Принцип простий: чим нижчий рівень, тим більше тестів і тим швидше вони виконуються. Чим вищий — тим менше, але більш реалістичних.
Конкретні числа від Mike Cohn: ~70% unit, ~20% integration, ~10% E2E. Але важливо розуміти: ці пропорції — орієнтир, не закон. Для мікросервісної архітектури, де багато "клею" між сервісами, частка інтеграційних тестів може бути вищою.
Unit test (модульний тест) — тест, що перевіряє найменшу ізольовану одиницю поведінки системи.
Але що є "одиницею" (unit)? Це один з найбільш дискусійних питань у тестуванні.
Мартін Фаулер запропонував терміни Solitary та Sociable unit tests:
| Тип | Опис | Коли використовувати |
|---|---|---|
| Solitary | Всі залежності замінені моками | Лондонська школа. Перевірка взаємодій |
| Sociable | Деякі залежності реальні (value objects, helpers) | Детройтська школа. Перевірка стану |
Про школи тестування — детальна стаття.
Добрий unit test відповідає акроніму FIRST:
Unit тест має виконуватись за мілісекунди. Якщо тест займає секунди — він, ймовірно, вже не unit: він звертається до бази, мережі або файлової системи.
Практичне правило: якщо 1000 unit тестів займають більше 10 секунд — щось не так.
DateTime.Now), від рандому, від зовнішнього API — не є відтворюваним.Уявіть складний механічний годинник. У ньому сотні шестерень. Unit тест — це коли ви виймаєте одну шестерню, кладете на стіл і перевіряєте: чи правильні зуби? чи правильний розмір? чи повертається у правильний бік?
Ви не перевіряєте, чи правильно показує час весь годинник — для цього є інтеграційний тест. Але ви хочете бути впевнені, що кожна шестерня правильна, перш ніж збирати всю систему.
Інтеграційний тест (integration test) перевіряє взаємодію між двома або більше компонентами. Він відповідає на питання: "Чи правильно ці компоненти спілкуються між собою?"
Мартін Фаулер розрізняє два підвиди інтеграційних тестів:
Narrow Integration Test (Вузький): тест однієї точки інтеграції з використанням test doubles (заглушок) для всіх інших залежностей. Наприклад, тест репозиторію з реальною базою даних, але з мокованим кешем.
Wide Integration Test (Широкий): тест реального взаємодії між компонентами без заміни залежностей. Наприклад, тест, що перевіряє повний шлях: HTTP запит → контролер → сервіс → репозиторій → BD.
WebApplicationFactory — це, власне, те, що ми розглянемо детально у статті про інтеграційне тестування. Він запускає весь додаток в пам'яті і дозволяє надсилати реальні HTTP-запити.Інтеграційні тести особливо цінні для виявлення цілого класу помилок, які unit тести пропускають:
Саме тому навіть при 100% unit test coverage можна мати серйозні баги — якщо немає інтеграційних тестів.
End-to-End тест (E2E, системний тест) перевіряє повний потік від початку до кінця, з позиції кінцевого користувача. Він взаємодіє із системою так, як це робив би реальний користувач: через UI браузера, через мобільний додаток, через публічне API.
E2E — найдорожчий і найповільніший тип тестів, тому їх кількість має бути мінімальною та стратегічною. Виправдати E2E тест можна, якщо:
Піраміда Кона — відмінна модель, але вона не єдина і не завжди підходить. Протягом останнього десятиліття з'явились альтернативні концепції.
Spotify розробив Honeycomb для своєї мікросервісної архітектури. Ключова ідея: основою є тести одного сервісу в ізоляції (з реальним HTTP, реальним DI, реальною логікою, але з мокованими зовнішніми сервісами). Unit тести використовуються вибірково для складної логіки. Повноцінні E2E — мінімально.
Причина: для мікросервісів unit тести покривають малу частину ризиків, а E2E між сервісами надто складні. "Service integration tests" — золота середина.
Kent C. Dodds (автор React Testing Library) запропонував модель для фронтенд-розробки. Ключова ідея: інтеграційні тести — основа, бо вони перевіряють поведінку компонента з точки зору користувача, без прив'язки до реалізації. Плюс додає рівень "Static Analysis" (TypeScript, ESLint) як перший захист.
Незважаючи на фронтенд-контекст, Trinity Corporation застосовна і до бекенду: інтеграційні тести через WebApplicationFactory займають центральне місце.
Оригінальна модель швейцарського сиру (Swiss Cheese Model) використовується в авіаційній безпеці і медицині. Ідея: кожен шар захисту має "дірки" (слабкі місця), але якщо шари розташовані правильно, жодна "дірка" не проходить наскрізь через всі шари.
У тестуванні: жоден рівень тестів не є досконалим. Unit тести пропустять помилки конфігурації. Інтеграційні пропустять деякі edge cases. E2E пропустять більшість деталей. Але комбінація кількох рівнів значно підвищує загальне покриття ризиків.
| Модель | Основа | Для кого | Ключова ідея |
|---|---|---|---|
| Pyramid (Cohn) | Unit | Класика, будь-які системи | Більше дешевих, менше дорогих |
| Honeycomb (Spotify) | Service integration | Мікросервіси | Сервіс як одиниця тестування |
| Trophy (Dodds) | Integration | Frontend/React | Поведінка > Реалізація |
| Swiss Cheese | Всі шари | Безпека-критичні системи | Шари захисту, що перекриваються |
| Що тестуємо | Unit | Integration | E2E |
|---|---|---|---|
| Бізнес-логіка (алгоритми) | ✅ Основне місце | — | — |
| Трансформація даних | ✅ | — | — |
| Валідація вхідних даних | ✅ | ✅ | — |
| DI-конфігурація | — | ✅ | — |
| Routing та middleware | — | ✅ | — |
| HTTP-серіалізація/десеріалізація | — | ✅ | — |
| SQL-запити (EF Core) | — | ✅ | — |
| Аутентифікація/авторизація | ✅ (логіка) | ✅ (middleware) | ✅ (flow) |
| Критичні user journeys | — | — | ✅ |
| Third-party інтеграції | — | ✅ (stub) | ✅ (real, вибірково) |
Мокування (використання test doubles) — інструмент ізоляції. Детально про нього — у статті про Moq.
| Рівень | Що РЕАЛЬНО | Що МОКУЄТЬСЯ |
|---|---|---|
| Unit | Один клас/метод | Всі зовнішні залежності (БД, HTTP, файли, час) |
| Integration (Narrow) | Один компонент + БД | Зовнішні HTTP-сервіси, email, SMS |
| Integration (Wide) | Весь стек вашого додатку | Зовнішні сервіси (payment gateway, etc.) |
| E2E | Весь стек | Може нічого (або лише платіжні системи) |
Алгоритм прийняття рішення: "Який тест мені потрібен?"
Розробники часто забувають, що тести — це код. Його треба підтримувати при рефакторингу, оновленні залежностей, зміні вимог.
| Аспект | Unit | Integration | E2E |
|---|---|---|---|
| Написати | Дешево (хвилини) | Середньо (години) | Дорого (дні) |
| Підтримувати | Дешево | Середньо | Дорого |
| Debug провалу | Просто (точне місце) | Середньо | Складно (де саме?) |
| Run time | Мілісекунди | Секунди | Хвилини |
| Flakiness | Низька | Середня | Висока |
| Реалізм | Низький | Середній | Високий |
У нашому курсі ми будемо будувати тестову стратегію, що відповідає такій структурі для Minimal API:
Це відповідає моделі, близькій до Honeycomb: сервіс (наш додаток) як одиниця інтеграційного тесту.
Рівень 1: Розуміння
Завдання 1.1 — Намалюйте власну "анти-піраміду" (Ice Cream Cone) і поясніть письмово, що відбудеться з командою з 5 розробників за рік, якщо вони слідуватимуть цій стратегії. Конкретно: скільки тестів, скільки часу на запуск, яка продуктивність.
Завдання 1.2 — Для кожного з 5 сценаріїв нижче оберіть тип тесту (Unit/Integration/E2E) та поясніть чому: (а) перевірка формули розрахунку знижки; (б) перевірка що POST /orders зберігає замовлення в БД; (в) перевірка що юзер може пройти повний flow реєстрації → підтвердження email → вхід → замовлення; (г) перевірка що JWT middleware повертає 401 для неправильного токена; (д) перевірка що LINQ запит генерує правильний SQL.
Завдання 1.3 — Знайдіть будь-який відкритий .NET проєкт на GitHub. Визначте яку тестову стратегію вони використовують (наскільки це можна зрозуміти). Чи відповідає вона піраміді?
Рівень 2: Аналіз
Завдання 2.1 — Ваша команда будує e-commerce API. Визначте для кожного компоненту системи (ProductService, OrderRepository, DiscountCalculator, AuthMiddleware, PaymentGatewayClient) оптимальний рівень тестування та обґрунтуйте. Що мокується в кожному випадку?
Завдання 2.2 — Порівняйте моделі Pyramid та Honeycomb для системи з 5 мікросервісів, де кожен сервіс викликає API двох інших. Яка модель краще підходить та чому? Намалюйте відповідну діаграму.
Рівень 3: Практика
Завдання 3.1 — Сплануйте тестову стратегію для реального проєкту. Візьміть будь-який pet project або учбовий проєкт. Створіть документ "Test Strategy" з: (а) вибраною моделлю піраміди, (б) конкретним списком що тестується на якому рівні, (в) інструментами для кожного рівня, (г) цільовим % coverage.
Завдання 3.2 — Розрахуйте приблизний час виконання повного тестового набору для двох стратегій для системи з 100 функцій: (а) 90% E2E, 10% Unit; (б) 70% Unit, 20% Integration, 10% E2E. Зробіть висновок про продуктивність команди.
Наступна стаття відповідає на питання, яке виникає після розуміння піраміди: "Якими конкретно мають бути тести на кожному рівні?" Йдеться про фундаментальну суперечку у тестуванні — Дві школи: Лондон vs Детройт.
Що таке тестування? Від інтуїції до науки
Глибокий теоретичний огляд тестування ПЗ як дисципліни — від катастроф через баги до академічних визначень, психології розробника та процесів ISTQB.
Дві Школи Тестування — Лондон проти Детройту
Глибокий теоретичний розбір двох фундаментально різних підходів до unit-тестування — Класичної (Детройтської) та Лондонської шкіл. Повна таксономія Test Doubles, порівняння State vs Behaviour verification, і практичні рекомендації коли яку школу застосовувати.