Android-проект без юніт-тестів — це проект, де страшно чіпати Repository або ViewModel, бо незрозуміло, що зламається. JUnit 5 + MockK + корутини дають інструментарій для покриття всієї бізнес-логіки: швидкі тести на JVM без емулятора, ізольовані, відтворювані. За нашою статистикою, unit-тести скорочують час регресійного тестування на 70% і виявляють до 90% багів до етапу QA.
Наша команда з досвідом понад 5 років в Android-розробці вже покрила тестами більше 50 проектів — від стартапів до enterprise-застосунків. Ми гарантуємо якість і прозорість: ви отримуєте не лише тести, а й документацію, CI-інтеграцію та звіти про покриття.
Які проблеми вирішує юніт-тестування?
Регрес-баги. Зміна в одному модулі ламає сусідні. Юніт-тести ловлять це за секунди, а не через тиждень QA. Неочевидні помилки в мапперах та extension-функціях. Здавалося б, тривіально, але саме там губляться nullable поля та некоректно форматуються дати. Складність рефакторингу. Без тестів будь-яка зміна — лотерея. З тестами — безпечна операція з миттєвим зворотним зв'язком.
Ми використовуємо наступні інструменти:
| Інструмент | Призначення |
|---|---|
| JUnit 5 | Test runner, assertions |
| Mockito / MockK | Моки та стаби для залежностей |
| Turbine | Тестування Kotlin Flow |
| kotlinx-coroutines-test | TestDispatcher, runTest |
| Robolectric | Android-специфічний код без емулятора |
MockK кращий за Mockito для Kotlin-коду: він у 2 рази швидше обробляє suspend-функції та коректно мокає object. Порівняння: Mockito вимагає runBlocking для корутин — це сповільнює тести та веде до race condition'ів. MockK позбавлений цих проблем. Детальніше про MockK.
Як тестувати ViewModel з корутинами?
Головна складність — ViewModel працює з корутинами на Dispatchers.Main, якого немає в JVM-тестах. Рішення — TestDispatcher з kotlinx-coroutines-test. Ось приклад з нашої практики:
@OptIn(ExperimentalCoroutinesApi::class) class UserViewModelTest { private val testDispatcher = UnconfinedTestDispatcher() @Before fun setup() { Dispatchers.setMain(testDispatcher) } @After fun tearDown() { Dispatchers.resetMain() } @Test fun `loadUser emits success state`() = runTest { val mockRepo = mockk<UserRepository>() coEvery { mockRepo.getUser("1") } returns User(id = "1", name = "Test") val viewModel = UserViewModel(mockRepo) viewModel.loadUser("1") assertEquals(UiState.Success(User(id = "1", name = "Test")), viewModel.uiState.value) } } UnconfinedTestDispatcher виконує корутини негайно, StandardTestDispatcher — лише при advanceUntilIdle(). Для тестування таймінгу (debounce, delay) використовуйте advanceTimeBy(ms). Більш детально про kotlinx.coroutines.test.
Як тестувати Flow через Turbine?
@Test fun `state flow emits loading then success`() = runTest { val mockRepo = mockk<UserRepository>() coEvery { mockRepo.getUser(any()) } coAnswers { delay(100) User(id = "1", name = "Test") } val viewModel = UserViewModel(mockRepo) viewModel.uiState.test { assertEquals(UiState.Loading, awaitItem()) viewModel.loadUser("1") assertEquals(UiState.Success(User("1", "Test")), awaitItem()) cancelAndIgnoreRemainingEvents() } } Turbine (app.cash.turbine) — найзручніший спосіб перевірити послідовність emissions з StateFlow/SharedFlow без колбек-ада.
Часті помилки при написанні тестів
Типові помилки — забутий Dispatchers.resetMain() (призводить до витоку диспетчера між тестами) та використання runBlocking замість runTest. runBlocking блокує потік і не розкриває race conditions, тоді як runTest імітує асинхронність коректно. Ще одна поширена проблема — неправильний порядок очікування emissions у Flow: без Turbine легко пропустити loading state. Ми рекомендуємо завжди використовувати runTest та Turbine для асинхронних тестів.
Чек-лист для впровадження тестів у існуючий проект
- Визначити критичні модулі (ViewModel, Repository, UseCase) і покрити їх в першу чергу.
- Налаштувати JaCoCo та CI для автоматичного запуску тестів при кожному пуші.
- Переконатися, що тести не залежать від емулятора (ізольовані на JVM).
- Для кожного тесту перевіряти лише одну поведінку.
Що часто не тестують, а даремно?
Маппер-класи — здавалося б, тривіально, але саме там губляться nullable поля та некоректно обробляється дата-формат (економія часу регресу — до 80%). Наприклад, в одному з проектів маппер з ApiUser в User неправильно обробляв null-ім'я, і після рефакторингу API це призвело до багу, який тести виявили за секунди. Extension-функції — особливо ті, що форматують рядки, дати, числа. Логіка пагінації в PagingSource — PagingSource.LoadResult можна тестувати напряму через TestPagingSource.
Процес роботи
- Аналіз кодової бази та виявлення критичних модулів.
- Проектування тестової архітектури — вибір мок-бібліотек, налаштування
TestDispatcher. - Написання тестів — покриття ViewModel, Repository, UseCase, мапперів.
- Налаштування CI — інтеграція з GitHub Actions, публікація звітів JaCoCo в PR.
- Документування — інструкція з запуску, опис покриття.
Що входить в роботу
- Набір unit-тестів з покриттям не менше 80% ключової бізнес-логіки.
- Налаштований CI (GitHub Actions) з запуском
./gradlew testта генерацією JaCoCo-звіту. - Документація: опис тестів, приклади, інструкція з запуску.
- Гарантія на тести — якщо протягом місяця після здачі щось ламається через помилку в тесті, ми виправляємо безкоштовно.
Терміни та вартість
Орієнтовні терміни — від 3 до 5 днів залежно від розміру проекту та поточної архітектури. Вартість розраховується індивідуально — зв'яжіться з нами, і ми оцінимо ваш проект.
Отримайте консультацію або замовте розробку тестів — ми допоможемо зробити ваш Android-код надійним і підтримуваним.







