Юніт-тестування — метод перевірки програмного забезпечення, при якому тестується коректність роботи окремих модулів або функцій коду в ізоляції від решти системи. За даними Martin Fowler, 2026, юніт-тести є фундаментом CI/CD та рефакторингу, забезпечуючи швидкий зворотний зв'язок про працездатність коду. Модульне тестування допомагає виявляти помилки на ранніх етапах розробки, знижуючи вартість їх виправлення в десятки разів.
Головне
Юніт-тестування — це процес перевірки окремих одиниць вихідного коду — функцій, методів, класів — в ізоляції від решти програми. Кожен тест запускає конкретний сценарій використання модуля та перевіряє, що результат відповідає очікуваному. Юніт-тести пишуться тією ж мовою програмування, що й основний код, і виконуються автоматично в середовищі розробки або в CI/CD пайплайні. На відміну від інтеграційних тестів, юніт-тести не взаємодіють з реальними базами даних, файловою системою або мережевими сервісами.
Основна мета — швидкий зворотний зв'язок про коректність коду після змін. Якщо розробник рефакторить метод, набір юніт-тестів підтверджує, що поведінка не зламалася. За даними Google Testing Blog (2025), проєкти з покриттям юніт-тестами вище 60% мають у 2.5 рази менше production-інцидентів. Додаткові переваги: документація коду (тести показують, як використовувати API), спрощення рефакторингу (можна змінювати реалізацію, зберігаючи поведінку) та швидка діагностика регресій.
Не кожен автоматичний тест — юніт-тест. Критерії: тестується один модуль (клас або функція), зовнішні залежності замінені моками або стабами, тест виконується за мілісекунди, не потребує підняття сервера або бази даних. Тест, що звертається до реальної бази даних — це інтеграційний тест. Тест, що відкриває браузер — E2E-тест. Розуміння меж між типами тестів важливе для правильного розподілу зусиль у піраміді тестування.
Якісні юніт-тести слідують принципам FIRST, сформульованим Robert C. Martin. Кожен тест має бути Fast (швидкий — мілісекунди), Isolated (ізольований — не залежить від інших тестів), Repeatable (відтворюваний — однаковий результат на будь-якій машині), Self-validating (самоперевірний — результат "passed" або "failed", без ручної перевірки) та Timely (своєчасний — написаний до або одночасно з кодом). Порушення будь-якого принципу знижує цінність тесту.
Стандартний шаблон для написання юніт-тестів. Arrange — підготовка даних та залежностей: створення об'єктів, налаштування моків, завдання вхідних параметрів. Act — виконання тестованої дії: виклик методу або функції. Assert — перевірка результату: порівняння фактичного значення з очікуваним. Розділення на три блоки робить тест читабельним та зрозумілим. Якщо блок Assert потребує складної логіки — тест, ймовірно, перевіряє забагато за один раз.
// Приклад юніт-тесту за шаблоном AAA на Kotlin з JUnit 5
class CalculatorTest {
private lateinit var calculator: Calculator
@BeforeEach
fun setUp() {
// ARRANGE — створюємо тестований об'єкт
calculator = Calculator()
}
@Test
fun addition_shouldReturnCorrectSum() {
// ACT — виконуємо дію
val result = calculator.add(2, 3)
// ASSERT — перевіряємо результат
Assertions.assertEquals(5, result)
}
}
Ім'я тесту має описувати, що перевіряється і який очікується результат. Формат: [methodName]_[scenario]_[expectedResult]. Приклад: calculateTotal_whenDiscountApplied_shouldReturnDiscountedTotal. Хороше ім'я тесту замінює коментар і при падінні одразу вказує, яка функціональність порушена. Уникайте імен на кшталт test1, checkSomething або verify — вони не несуть інформації та ускладнюють діагностику.
Для ізоляції тестованого модуля від зовнішніх залежностей використовуються тестові дублери (test doubles). Основні типи: моки (mocks) — перевіряють, що певний метод був викликаний з потрібними параметрами; стаби (stubs) — повертають задані значення при виклику методу; фейки (fakes) — спрощена реалізація реального компонента (наприклад, InMemoryUserRepository замість UserRepository, що працює з БД). Вибір залежить від того, що потрібно перевірити: стан (стаб) чи взаємодію (мок).
| Дублер | Що перевіряє | Приклад |
|---|---|---|
| Мок | Виклик методу з правильними параметрами | userRepository.save(user) був викликаний рівно 1 раз |
| Стаб | Значення, що повертається | repository.findById(1) повертає User(id=1, name="Test") |
| Фейк | Логіка через спрощену реалізацію | InMemoryMapUserRepository з HashMap замість БД |
| Шпигун | Частковий мокінг реального об'єкта | spy(repo).when(findById).thenReturn(user) |
Mockito — найпопулярніший фреймворк для мокінгу в Java та Kotlin. Дозволяє створювати моки через mock(), налаштовувати повернення значень через when().thenReturn() та перевіряти виклики через verify(). Сучасні версії Mockito (5.x) підтримують статичні моки (mockStatic) та спрощений синтаксис через BDDMockito (given-willReturn). Важливе правило: не мокайте те, що не ваше — не створюйте моки для value-об'єктів та стандартних бібліотек.
// Приклад юніт-тесту з Mockito на Kotlin
class OrderServiceTest {
@Mock
private lateinit var paymentGateway: PaymentGateway
@Mock
private lateinit var userRepository: UserRepository
private lateinit var orderService: OrderService
@BeforeEach
fun init() {
MockitoAnnotations.openMocks(this)
orderService = OrderService(paymentGateway, userRepository)
}
@Test
fun processOrder_whenPaymentFails_shouldThrowException() {
// given
val user = User(id = 1, balance = 100.0)
val order = Order(amount = 200.0)
Mockito.`when`(paymentGateway.charge(any())).thenReturn(false)
Mockito.`when`(userRepository.findById(1)).thenReturn(user)
// when & then
assert Throws<PaymentException> {
orderService.processOrder(user.id, order)
}
// verify
Mockito.verify(paymentGateway).charge(any())
}
}
TDD (Test-Driven Development) — методологія, при якій тест пишеться до реалізації коду. Цикл "Red-Green-Refactor": написати тест, який падає (Red), написати мінімальний код для проходження тесту (Green), покращити код без зміни поведінки (Refactor). TDD гарантує, що весь код покритий тестами (coverage = 100% для написаного функціоналу) і що код тестований — якщо код складно протестувати, значить, архітектура потребує покращення.
За даними дослідження IBM (2006-2026, лонгітюдне дослідження), команди, які використовують TDD, допускають на 40-80% менше дефектів у production порівняно з командами, що пишуть тести після коду. TDD також покращує архітектуру: розробник змушений думати про дизайн API до реалізації, що призводить до слабкої зв'язаності (loose coupling) та високої згуртованості (high cohesion). Додатковий ефект — документація живим кодом: тести слугують специфікацією поведінки модуля, яка завжди актуальна.
TDD не завжди оптимальний. UI-компоненти складно тестувати в ізоляції — для них ефективніші snapshot-тести або візуальне регресійне тестування (Percy, Chromatic). Прототипування та дослідження (spike solutions) не потребують тестів. Legacy-код без тестів складно покрити через TDD — тут спочатку потрібні characterization tests (тести, що фіксують поточну поведінку перед рефакторингом). У цих випадках TDD не скасовується повністю, а адаптується — пишуться тести на змінюваний функціонал, а не на весь legacy-код.
Мобільна розробка має специфіку: бізнес-логіка часто змішана з UI-кодом (Activity, ViewController, ViewModel), що ускладнює юніт-тестування. Найкраща практика — тонкі View, товсті ViewModel: виносьте всю логіку з UI-компонентів в окремі класи (UseCase, Repository, ViewModel), які легко тестувати без емулятора. Для Android та iOS існують нативні фреймворки юніт-тестування, що працюють на JVM/Native без запуску пристрою.
Android юніт-тести виконуються на локальній JVM без емулятора, що забезпечує швидкість виконання — типовий тест займає < 100ms. JUnit 5 — основний runner. Для ViewModel-тестів використовуйте kotlinx-coroutines-test для тестування корутин та Turbine для тестування StateFlow. Robolectric дозволяє тестувати Android-залежні компоненти (Context, Resources) без емулятора, завантажуючи shadow-класи. Для Compose-тестів використовуйте Compose UI Test — але це вже UI-тести, не юніт.
iOS юніт-тести пишуться на Swift з XCTest (вбудований в Xcode). Quick + Nimble — BDD-фреймворки для більш читабельних тестів (describe/context/it). Для мокінгу використовуйте Cuckoo (генерація моків) або SwiftyMocky. Swift підтримує протоколи та dependency injection, що полегшує заміну залежностей. Key point: iOS юніт-тести виконуються на симуляторі macOS, а не на реальному пристрої. Тести, що потребують апаратних функцій (камера, Bluetooth), — це інтеграційні тести.
Flutter юніт-тести використовують пакет flutter_test та виконуються на Dart VM без емулятора. Для мокінгу — пакет mockito з генератором коду (build_runner). Widget-тести (в тому ж пакеті) тестують окремі віджети, але потребують рендерингу і працюють повільніше — використовуйте їх тільки для перевірки UI-логіки. Чиста Dart-логіка (моделі, репозиторії, блоки) тестується як звичайні Dart-тести без імпорту flutter_test.
// Приклад юніт-тесту на Flutter з mockito
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';
@GenerateMocks([ApiClient])
import 'user_repository_test.mocks.dart';
void main() {
late MockApiClient mockApi;
late UserRepository repository;
setUp(() {
mockApi = MockApiClient();
repository = UserRepository(mockApi);
});
test('fetchUser returns user when API succeeds', () async {
// Arrange
final expectedUser = User(id: 1, name: 'Test');
when(mockApi.getUser(1))
.thenAnswer((_) async => expectedUser);
// Act
final result = await repository.fetchUser(1);
// Assert
expect(result, expectedUser);
verify(mockApi.getUser(1)).called(1);
});
}
Ефективне юніт-тестування потребує дисципліни. Головне правило: тестуйте поведінку, а не реалізацію. Тест не повинен знати, як модуль реалізований всередині (які приватні методи викликаються, в якому порядку). Якщо тест прив'язаний до реалізації, він ламається при кожному рефакторингу і втрачає цінність. Тест перевіряє контракт: при вході X має бути вихід Y. Виняток — тести для алгоритмів з критичною продуктивністю, де важлива послідовність викликів.
100% coverage — недосяжна і не потрібна мета. За даними Google Testing Blog (2025), оптимальний рівень покриття для юніт-тестів — 70-80% рядків коду. 100% coverage часто досягається за рахунок тестування геттерів, сеттерів і конструкторів, що не приносить цінності. Фокусуйтеся на критичній бізнес-логіці: складні обчислення, валідація, обробка помилок, edge-cases. Використовуйте JaCoCo (Java), Coverage.py (Python), Istanbul (JS) для вимірювання та налаштуйте поріг у CI — падіння збірки при coverage < 60%.
Юніт-тести — перша стадія будь-якого CI/CD пайплайну. Вони виконуються при кожному пуші в репозиторій, до збірки та деплою. Середній час прогону юніт-тестів у проєкті не має перевищувати 5 хвилин — якщо довше, тести перестають бути "швидкими" і розробники перестають їх запускати локально. Розділяйте тести на швидкі (юніт) та повільні (інтеграційні) і запускайте їх у різних стейджах пайплайну. Використовуйте parallel execution та fail-fast для прискорення.
Часті запитання
Юніт-тест перевіряє один модуль в ізоляції, замінюючи зовнішні залежності моками. Інтеграційний тест перевіряє взаємодію між кількома реальними компонентами (БД, API, файлова система). Юніт-тести виконуються за мілісекунди, інтеграційні — за секунди. У піраміді тестування юніт-тести займають 70%.
Вибір залежить від платформи: JUnit 5 для Java/Kotlin, XCTest для iOS/Swift, pytest для Python, Jest/Vitest для JavaScript/TypeScript, flutter_test для Flutter. Для мокінгу використовуйте Mockito (Java), Cuckoo (iOS), unittest.mock (Python) або vitest.mock (JS). Всі сучасні фреймворки підтримують параметризовані тести, вбудовані assertions та паралельний запуск.
Fast — тест виконується за мілісекунди. Isolated — не залежить від інших тестів та зовнішніх систем. Repeatable — дає однаковий результат на будь-якій машині. Self-validating — перевіряє результат автоматично. Timely — написаний до або синхронно з кодом. Порушення хоча б одного принципу знижує ефективність тестування.
Так, обов'язково. ViewModel містить бізнес-логіку — обробку подій, трансформацію даних, управління станом. На Android використовуйте kotlinx-coroutines-test для корутин та Turbine для тестування StateFlow. На iOS тестуйте Combine Publishers або async/await у ViewModel. ViewModel-тести — це чисті юніт-тести, що працюють на JVM/macOS без емулятора.
Мережеві запити в юніт-тестах не виконуються — вони замінюються моками HTTP-клієнта. На Android використовуйте MockWebServer (OkHttp) — він піднімає локальний HTTP-сервер, що краще за моки, оскільки відтворює реальну мережеву взаємодію. MockWebServer дає ізоляцію без втрати реалістичності. Для iOS — OHHTTPStubs або URLProtocol для перехоплення та підміни відповідей.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також