Юніт-тестування в мобільній розробці: що це, методи та фреймворки

Автор: IT Sectr Опубліковано: 2026-04-06 Час читання: 9 хв

Юніт-тестування — метод перевірки програмного забезпечення, при якому тестується коректність роботи окремих модулів або функцій коду в ізоляції від решти системи. За даними Martin Fowler, 2026, юніт-тести є фундаментом CI/CD та рефакторингу, забезпечуючи швидкий зворотний зв'язок про працездатність коду. Модульне тестування допомагає виявляти помилки на ранніх етапах розробки, знижуючи вартість їх виправлення в десятки разів.

Головне

  • Юніт-тест — перевірка одного модуля (функції, методу, класу) в ізоляції від зовнішніх залежностей
  • Принципи FIRST — Fast, Isolated, Repeatable, Self-validating, Timely — основа якісних тестів
  • Моки та стаби — замінники зовнішніх залежностей (БД, API, файлова система), що забезпечують ізоляцію тесту
  • TDD (Test-Driven Development) — методологія розробки через тестування: червоний-зелений-рефакторинг
  • Піраміда тестів — юніт-тести займають 70% піраміди, забезпечуючи швидкий фідбек на кожному коміті

Що таке юніт-тестування?

Юніт-тестування — це процес перевірки окремих одиниць вихідного коду — функцій, методів, класів — в ізоляції від решти програми. Кожен тест запускає конкретний сценарій використання модуля та перевіряє, що результат відповідає очікуваному. Юніт-тести пишуться тією ж мовою програмування, що й основний код, і виконуються автоматично в середовищі розробки або в CI/CD пайплайні. На відміну від інтеграційних тестів, юніт-тести не взаємодіють з реальними базами даних, файловою системою або мережевими сервісами.

Навіщо потрібні юніт-тести?

Основна мета — швидкий зворотний зв'язок про коректність коду після змін. Якщо розробник рефакторить метод, набір юніт-тестів підтверджує, що поведінка не зламалася. За даними Google Testing Blog (2025), проєкти з покриттям юніт-тестами вище 60% мають у 2.5 рази менше production-інцидентів. Додаткові переваги: документація коду (тести показують, як використовувати API), спрощення рефакторингу (можна змінювати реалізацію, зберігаючи поведінку) та швидка діагностика регресій.

Що вважається юніт-тестом?

Не кожен автоматичний тест — юніт-тест. Критерії: тестується один модуль (клас або функція), зовнішні залежності замінені моками або стабами, тест виконується за мілісекунди, не потребує підняття сервера або бази даних. Тест, що звертається до реальної бази даних — це інтеграційний тест. Тест, що відкриває браузер — E2E-тест. Розуміння меж між типами тестів важливе для правильного розподілу зусиль у піраміді тестування.

Принципи FIRST та структура AAA

Якісні юніт-тести слідують принципам FIRST, сформульованим Robert C. Martin. Кожен тест має бути Fast (швидкий — мілісекунди), Isolated (ізольований — не залежить від інших тестів), Repeatable (відтворюваний — однаковий результат на будь-якій машині), Self-validating (самоперевірний — результат "passed" або "failed", без ручної перевірки) та Timely (своєчасний — написаний до або одночасно з кодом). Порушення будь-якого принципу знижує цінність тесту.

Структура AAA (Arrange-Act-Assert)

Стандартний шаблон для написання юніт-тестів. Arrange — підготовка даних та залежностей: створення об'єктів, налаштування моків, завдання вхідних параметрів. Act — виконання тестованої дії: виклик методу або функції. Assert — перевірка результату: порівняння фактичного значення з очікуваним. Розділення на три блоки робить тест читабельним та зрозумілим. Якщо блок Assert потребує складної логіки — тест, ймовірно, перевіряє забагато за один раз.

kotlin
// Приклад юніт-тесту за шаблоном 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

Mockito — найпопулярніший фреймворк для мокінгу в Java та Kotlin. Дозволяє створювати моки через mock(), налаштовувати повернення значень через when().thenReturn() та перевіряти виклики через verify(). Сучасні версії Mockito (5.x) підтримують статичні моки (mockStatic) та спрощений синтаксис через BDDMockito (given-willReturn). Важливе правило: не мокайте те, що не ваше — не створюйте моки для value-об'єктів та стандартних бібліотек.

kotlin
// Приклад юніт-тесту з 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: розробка через тестування

TDD (Test-Driven Development) — методологія, при якій тест пишеться до реалізації коду. Цикл "Red-Green-Refactor": написати тест, який падає (Red), написати мінімальний код для проходження тесту (Green), покращити код без зміни поведінки (Refactor). TDD гарантує, що весь код покритий тестами (coverage = 100% для написаного функціоналу) і що код тестований — якщо код складно протестувати, значить, архітектура потребує покращення.

Переваги TDD

За даними дослідження IBM (2006-2026, лонгітюдне дослідження), команди, які використовують TDD, допускають на 40-80% менше дефектів у production порівняно з командами, що пишуть тести після коду. TDD також покращує архітектуру: розробник змушений думати про дизайн API до реалізації, що призводить до слабкої зв'язаності (loose coupling) та високої згуртованості (high cohesion). Додатковий ефект — документація живим кодом: тести слугують специфікацією поведінки модуля, яка завжди актуальна.

Коли TDD не підходить?

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 (JUnit + Mockito/Robolectric)

Android юніт-тести виконуються на локальній JVM без емулятора, що забезпечує швидкість виконання — типовий тест займає < 100ms. JUnit 5 — основний runner. Для ViewModel-тестів використовуйте kotlinx-coroutines-test для тестування корутин та Turbine для тестування StateFlow. Robolectric дозволяє тестувати Android-залежні компоненти (Context, Resources) без емулятора, завантажуючи shadow-класи. Для Compose-тестів використовуйте Compose UI Test — але це вже UI-тести, не юніт.

Юніт-тести на iOS (XCTest + Quick/Nimble)

iOS юніт-тести пишуться на Swift з XCTest (вбудований в Xcode). Quick + Nimble — BDD-фреймворки для більш читабельних тестів (describe/context/it). Для мокінгу використовуйте Cuckoo (генерація моків) або SwiftyMocky. Swift підтримує протоколи та dependency injection, що полегшує заміну залежностей. Key point: iOS юніт-тести виконуються на симуляторі macOS, а не на реальному пристрої. Тести, що потребують апаратних функцій (камера, Bluetooth), — це інтеграційні тести.

Юніт-тести на Flutter (flutter_test + Mockito)

Flutter юніт-тести використовують пакет flutter_test та виконуються на Dart VM без емулятора. Для мокінгу — пакет mockito з генератором коду (build_runner). Widget-тести (в тому ж пакеті) тестують окремі віджети, але потребують рендерингу і працюють повільніше — використовуйте їх тільки для перевірки UI-логіки. Чиста Dart-логіка (моделі, репозиторії, блоки) тестується як звичайні Dart-тести без імпорту flutter_test.

dart
// Приклад юніт-тесту на 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. Виняток — тести для алгоритмів з критичною продуктивністю, де важлива послідовність викликів.

  • Одна перевірка на тест — один assert або одна група пов'язаних assert'ів на одну логічну перевірку
  • Уникайте дублювання — використовуйте @BeforeEach / setUp для спільної ініціалізації, параметризовані тести для різних вхідних даних
  • Не тестуйте приватні методи — тестуйте через публічний API. Якщо приватний метод не покритий — значить, його логіка не видна ззовні
  • Покривайте граничні випадки — порожні колекції, null/undefined, від'ємні числа, максимальні значення
  • Не використовуйте Thread.sleep в тестах — це робить тести повільними і нестабільними. Використовуйте тестові тайм-аути та корутини

Який coverage вважати достатнім?

100% coverage — недосяжна і не потрібна мета. За даними Google Testing Blog (2025), оптимальний рівень покриття для юніт-тестів — 70-80% рядків коду. 100% coverage часто досягається за рахунок тестування геттерів, сеттерів і конструкторів, що не приносить цінності. Фокусуйтеся на критичній бізнес-логіці: складні обчислення, валідація, обробка помилок, edge-cases. Використовуйте JaCoCo (Java), Coverage.py (Python), Istanbul (JS) для вимірювання та налаштуйте поріг у CI — падіння збірки при coverage < 60%.

CI/CD та юніт-тести

Юніт-тести — перша стадія будь-якого 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 та паралельний запуск.

Що таке F.I.R.S.T. принципи тестування?

Fast — тест виконується за мілісекунди. Isolated — не залежить від інших тестів та зовнішніх систем. Repeatable — дає однаковий результат на будь-якій машині. Self-validating — перевіряє результат автоматично. Timely — написаний до або синхронно з кодом. Порушення хоча б одного принципу знижує ефективність тестування.

Чи потрібно писати юніт-тести для ViewModel в Android/iOS?

Так, обов'язково. 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 для перехоплення та підміни відповідей.

Підсумки

  • Юніт-тестування — перевірка окремих модулів в ізоляції від зовнішніх залежностей зі швидким зворотним зв'язком
  • Структура AAA — Arrange (підготовка), Act (дія), Assert (перевірка) — стандартний шаблон тесту
  • Моки та стаби — тестові дублери для ізоляції: моки перевіряють виклики, стаби повертають значення
  • TDD — розробка через тестування (Red-Green-Refactor) знижує кількість дефектів на 40-80%
  • Принципи FIRST — Fast, Isolated, Repeatable, Self-validating, Timely — основа якісного тесту
  • Платформенні інструменти — JUnit 5 (Android), XCTest (iOS), flutter_test (Flutter), Jest (React Native)
  • Покриття 70-80% — оптимальний рівень для критичної бізнес-логіки, геттери та сеттери не потребують тестів

Ми розробимо мобільний застосунок під ключ

IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

Читайте також