Unit-тестування Android-застосунків: що це і як провести?
Unit-тестування – один із ключових інструментів якості в Android-розробці. За даними MoldStud (2025), команди, що застосовують моки для ізоляції залежностей, запускають тести до 70% швидше порівняно з тестами, які звертаються до реальних мережевих шарів. При цьому 30% зниження дефектів фіксується у командах, які інтегрували тести в CI/CD-пайплайн. У цій статті – практичний посібник про юніт-тести: від базових понять до написання першого тесту з MockK і JUnit.
Що таке unit-тестування в Android?
Юніт-тести – це перевірка окремої одиниці коду: однієї функції, методу або класу – у повній ізоляції від решти системи. В Android такою «одиницею» найчастіше виступає ViewModel, UseCase або Repository. Unit-тест не запускає емулятор, не звертається до бази даних і не робить мережевих запитів – він працює лише з логікою конкретного класу, а всі залежності замінюються мок-об'єктами.
| Тип тесту | Що перевіряє | Де запускається | Швидкість |
|---|---|---|---|
| Unit-тест | Логіку одного класу/методу | JVM (без Android) | Мілісекунди |
| Інтеграційний | Взаємодію кількох компонентів | JVM / емулятор | Секунди |
| UI (Espresso) | Поведінку інтерфейсу з погляду користувача | Емулятор / пристрій | Секунди-хвилини |
Приклад: є LoginViewModel, який приймає email і пароль, валідує їх і викликає AuthRepository. Unit-тест перевіряє лише логіку валідації – без реального репозиторію і без UI. AuthRepository замінюється моком, який повертає потрібний результат.
Навіщо потрібні unit-тести в Android-розробці?
Навіщо потрібні unit-тести – питання, яке ставлять на початку проєкту і перестають ставити після першого великого рефакторингу без покриття. Ось п'ять конкретних причин:
- Швидкий фідбек. Тест на JVM виконується за мілісекунди – на відміну від збірки та запуску на емуляторі. Розробник отримує сигнал про баг одразу після зміни коду, а не через 2-3 хвилини збірки.
- Безпечний рефакторинг. Якщо всі unit-тести проходять після змін – поведінка системи не порушена. Без тестів рефакторинг перетворюється на вгадування: що зламалося, а що ні.
- Документація через код. Добре написаний тест – це специфікація поведінки класу. Новий розробник читає тест і розуміє, що має робити метод, без потреби читати всю бізнес-логіку. Це особливо цінно у великих Android-проєктах, де правильна архітектура інтерфейсу закладається з самого початку – детальніше про це в матеріалі «Інтерфейс мобільних застосунків: від А до Я».
- Зниження вартості багів. Баг, знайдений на етапі юніт-тесту, дешевший у виправленні в 5–10 разів порівняно з багом, знайденим у продакшні (за даними IBM NIST Study, класичний показник вартості дефекту за фазою).
- CI/CD-інтеграція. Unit-тести запускаються автоматично при кожному пул-реквесті. Це запобігає мержу коду, який ламає наявну логіку, без участі людини в перевірці.
Які види тестів існують і де місце unit-тестів?
Unit-тести – це основа тестової піраміди: їх має бути найбільше, вони мають виконуватися найшвидше і покривати максимальну частину бізнес-логіки.
Google рекомендує співвідношення 70/20/10: 70% unit-тестів, 20% інтеграційних, 10% UI. На практиці багато команд починають з нуля і рухаються до цього співвідношення ітеративно, починаючи з покриття ViewModel і UseCase.
Правило: якщо тест потребує запуску на емуляторі або пристрої – це вже не unit-тест. Unit-тест працює виключно на JVM, без Android-залежностей. Все, що потребує Context, Activity або Fragment, тестується через Robolectric або в androidTest.
Що тестувати, а що – ні?
Unit-тести в контексті Android – це тести бізнес-логіки, а не фреймворку. Порада: тестуйте те, що написали ви, а не те, що написав Google або JetBrains.
| Тестувати | Не тестувати |
|---|---|
| Логіку ViewModel (стан, події) | Стандартні класи SDK (Activity, Fragment) |
| UseCase / Interactor | Сторонні бібліотеки (Retrofit, Room) |
| Маппінг даних (DTO → Domain) | Автогенерований код |
| Валідацію форм і бізнес-правила | Прості гетери/сетери без логіки |
| Обробку помилок і edge cases | Константи й data-класи без поведінки |
Приклад хорошого кандидата для тесту: метод calculateDiscount(price, userType) у вашому прайсинговому UseCase. Приклад поганого кандидата: гетер getName() у data-класі User без жодної логіки.
Інструменти для unit-тестування Android-застосунків
Стандартний стек для unit-тестування Android у 2024-2025 році:
| Інструмент | Призначення | Коли використовувати |
|---|---|---|
| JUnit 4 / JUnit 5 | Базовий фреймворк: @Test, @Before, @After, assertions | Завжди – основа будь-якого unit-тесту |
| Mockito + mockito-kotlin | Створення моків, стаббінг, верифікація викликів | Java/Kotlin-проєкти, стандартний мокінг |
| MockK | Мокінг-фреймворк для Kotlin: object, companion, coroutines | Kotlin-проєкти, тести корутин і Flow |
| Turbine | Тестування Kotlin Flow і StateFlow | Коли ViewModel використовує Flow |
| kotlinx-coroutines-test | TestDispatcher, runTest для тестів корутин | Будь-який асинхронний код на корутинах |
| Robolectric | JVM-симуляція Android-середовища без емулятора | Коли потрібен Context, але не UI |
Залежності для build.gradle (Kotlin DSL):
testImplementation("junit:junit:4.13.2")
testImplementation("io.mockk:mockk:1.13.12")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1")
testImplementation("app.cash.turbine:turbine:1.1.0")
Офіційна документація з локальних тестів на сторінці developer.android.com/training/testing/local-tests. Детальне порівняння MockK і Mockito для Kotlin: MockK та Mockito.
Структура проєкту: куди писати тести?
Unit-тести Android-проєкту живуть у строго визначених директоріях – і плутати їх не можна:
| Директорія | Тип тестів | Де запускається | Приклад |
|---|---|---|---|
| app/src/test/java | Unit-тести (local tests) | JVM, без Android | ViewModelTest, UseCaseTest |
| app/src/androidTest/java | Інструментальні тести | Емулятор / пристрій | EspressoTest, RoomTest |
app/src/test/java – тут живуть unit-тести. Вони не потребують Android SDK, запускаються на чистій JVM. Файл LoginViewModelTest.kt має бути саме тут. Якщо випадково покласти unit-тест в androidTest – він вимагатиме емулятор і працюватиме в 10–20 разів повільніше.
app/src/androidTest/java – інструментальні тести: Espresso, тести Room із реальною базою, інтеграційні тести через Hilt. Вони повільніші, але дають змогу тестувати компоненти, які неможливо перевірити без Android-середовища.
Правило: якщо тест можна написати без Context і без Android-залежностей – він належить до test/java. Якщо ні – до androidTest/java.
Як написати unit-тест?
Розберемо написання unit-тестів на прикладі ViewModel із репозиторієм. Припустімо, є UserViewModel, який завантажує користувача за ID через UserRepository. Написання unit-тестів починається з паттерну AAA: Arrange → Act → Assert.
class UserViewModelTest {
private val repository = mockk()
private lateinit var viewModel: UserViewModel
@Before
fun setup() {
viewModel = UserViewModel(repository)
}
@Test
fun `loadUser emits user when repository returns success`() = runTest {
// Arrange
val user = User(id = 1, name = "Anna")
coEvery { repository.getUser(1) } returns Result.success(user)
// Act
viewModel.loadUser(1)
// Assert
assertEquals(user, viewModel.uiState.value.user)
}
}
Що тут відбувається: створюється мок репозиторію через mockk<>(). У методі setup() ініціалізується ViewModel із мок-залежністю. У тесті через coEvery задається поведінка мока – що повернути при виклику getUser(1). Потім викликається метод, що тестується, і через assertEquals перевіряється результат.
Як писати unit-тести для Flow і StateFlow – використовуйте turbine.awaitItem(). Замість assertEquals на .value збирайте емісії через turbine.awaitItem():
viewModel.uiState.test {
val item = awaitItem()
assertEquals(user, item.user)
cancelAndIgnoreRemainingEvents()
}
Часті помилки при написанні unit-тестів
Модульне тестування – це не просто запис коду з анотацією @Test. Типові помилки, які знижують цінність тестів:
| Помилка | Наслідок | Як правильно |
| Тестувати реалізацію, а не поведінку | Тест ламається при будь-якому рефакторингу, навіть якщо поведінка не змінилася | Перевіряйте вхідні дані → вихідні дані, а не порядок викликів методів |
| Використовувати relaxed = true в MockK скрізь | Пропущені NPE та приховані баги – мок мовчки повертає дефолтні значення | Явно задавайте поведінку через coEvery/every для кожного виклику |
| Один тест перевіряє кілька сценаріїв | При падінні незрозуміло, що зламалося; тест складно читати | Один тест – один сценарій. Використовуйте параметризовані тести для схожих випадків |
| Не перевіряти edge cases і помилки | Продакшн падає на порожніх списках, null і мережевих помилках | Завжди пишіть тести на success + error + empty state |
| Класти unit-тести в androidTest/ | Тести запускаються на емуляторі – повільно, нестабільно, не в CI | Unit-тести – лише в src/test/java, без Android-залежностей |
Висновок
Юніт-тестування – це інвестиція, яка окупається з кожним рефакторингом, новою фічею та виправленням бага. Правильно вибудований стек (JUnit + MockK + Turbine + coroutines-test) і чіткий розподіл test/ vs androidTest/ роблять написання тестів швидким і передбачуваним. Починайте з ViewModel і UseCase – це принесе максимальний ефект за мінімальних зусиль. Якщо ви плануєте Android-застосунок з нуля і хочете закласти правильну архітектуру з першого дня, ознайомтеся з варіантами розробки мобільних застосунків на Android під ключ.
Так – особливо в невеликому. Чим менший проєкт, тим швидше написати тести і тим болючіший кожен не виявлений баг. Мінімальний набір: тест для кожного UseCase і ViewModel. Навіть 20–30 тестів дають впевненість при рефакторингу. Корисний огляд інструментів для розробки Android/iOS з нуля – у матеріалі «Топ-10 програм для створення застосунків на Android та iOS».
Після проходження всіх тестів і фінальної збірки – в Google Play. Покроковий процес публікації описано в матеріалі «Як опублікувати застосунок у Google Play». Якщо потрібна допомога з архітектурою, тестуванням і випуском – розробка застосунків під ключ включає всі етапи від проєктування до публікації.




