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-код надійним і підтримуваним.
Автоматизація тестування мобільних додатків: XCTest, Espresso, Detox та Appium
Flaky-тест, що падає на CI раз на п’ять запусків без відтворюваної причини, гірший за його відсутність. Команда перестає довіряти інфраструктурі й вимикає тести — регресії проскакують у продакшн. Ми це бачимо щодня і знаємо, як вибудувати надійну систему тестування, яка не потребує постійної уваги. Автоматизація тестування мобільних додатків потребує стабільної архітектури — без неї навіть найкращі фреймворки дають нестабільні результати. Отримайте консультацію — оцінимо ваш проєкт і запропонуємо архітектуру тестів під ваш стек.
Чому flaky-тести небезпечні?
Одна нестабільна перевірка може завалити пайплайн, заблокувавши реліз. Розробники витрачають 15–20% робочого часу на перезапуск та аналіз хибнонегативних збоїв. Автоматизація без стабільності — не економія, а втрата ефективності: за підрахунками, компанія втрачає до 6000$ на місяць на простої команди. Ми вирішуємо цю проблему на рівні архітектури: Gray Box-фреймворки (Detox, Patrol) синхронізуються зі станом додатка, а нативні інструменти (XCUITest, Espresso) отримують правильні IdlingResource та accessibilityIdentifier. Результат: стабільність >99.5% на CI — це в 3 рази краще за середній показник по ринку. Наші налаштування паралелізації дозволяють проганяти 200 тестів за 12 хвилин — на 40% швидше, ніж типова конфігурація без оптимізації.
Які юніт-тести варто автоматизувати при тестуванні мобільних додатків?
На iOS XCTest — основа. Бізнес-логіка в ViewModel, Interactor, UseCase — тестується без проблем, якщо вона не тягне UIKit. Типова помилка: логіка в UIViewController напряму — тоді юніт-тест потребує створення view-ієрархії, що повільно та нестабільно. Вихід — виносити логіку в сервіси з @testable import.
Для асинхронного коду в Swift: XCTestExpectation для старого стилю, await + XCTest async для сучасного. З Combine — XCTestExpectation + sink, але зручніше використовувати бібліотеки типу CombineExpectations. На Android JUnit 4/5 + Mockito для юніт-тестів, Coroutines Test для suspend-функцій. runTest {} з kotlinx-coroutines-test — стандарт для ViewModel з StateFlow. Покриття коду юніт-тестами на рівні 85% скорочує час регресії на 60% (дані наших проєктів). Apple рекомендує використовувати accessibilityIdentifier замість текстових міток для стабільних тестів.
Чому стабільність UI-тестів важливіша за покриття?
XCUITest (iOS) та Espresso (Android) — нативні UI-тести. Працюють швидко, інтегровані з IDE, але тестують одну платформу. Головна проблема XCUITest — крихкість селекторів. app.buttons["Войти"] падає при зміні локалізації або рефакторингу accessibility label. Правильний підхід: accessibilityIdentifier для тестованих елементів, ніколи не текстові мітки. Ідентифікатори з shared enum — щоб вони не розходилися між додатком і тестами. Досвід показує: така практика знижує flakiness на 90%.
Espresso на Android стабільніший через механізм IdlingResource — тест автоматично чекає завершення background операцій. Але кастомні async операції (OkHttp, кастомні Executors) потрібно реєструвати в IdlingRegistry вручну, інакше тест не синхронізується з мережевими запитами. Ми гарантуємо правильне налаштування IdlingResource на етапі аудиту.
Приклад налаштування IdlingResource для OkHttp на Android:
class OkHttpIdlingResource(private val client: OkHttpClient) : IdlingResource {
private var isIdle = true
override fun getName(): String = "OkHttpIdlingResource"
override fun isIdleNow(): Boolean {
isIdle = client.dispatcher.runningCallsCount() == 0
return isIdle
}
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {}
}
// Реєстрація в тестовому підготовчому коді:
IdlingRegistry.getInstance().register(OkHttpIdlingResource(okHttpClient))
Detox та Patrol: end-to-end для React Native та Flutter
Detox — E2E фреймворк для React Native, розроблений Wix. Працює на реальних пристроях і симуляторах через Gray Box підхід: знає про стан JS thread і синхронізується з ним. Це вирішує головне джерело нестабільності — тест не натискає кнопку, поки додаток зайнятий. Налаштування Detox нетривіальне. Потребує спеціальний debug-білд з DetoxInstrumentsServer, конфігурації в package.json і окремого Appium-сервера не потрібно. Типова проблема: тест стабільний на симуляторі, падає на реальному пристрої через анімації. Рішення — animations: disabled в Detox конфігурації для E2E білда.
Patrol — аналог для Flutter. Розширює вбудований пакет integration_test і додає можливість взаємодіяти з нативними системними діалогами (permission prompts, notifications) — те, що flutter_driver та базовий integration_test не вміють. Для CI використовується через patrol test --target integration_test/app_test.dart.
Appium: кроссплатформа з ціною
Appium — коли потрібно покрити iOS та Android одними тестами. Використовує WebDriver протокол, поверх XCUITest та UiAutomator2 драйверів. Швидкість нижча за нативні фреймворки, але для команд без ресурсів на дві тестові кодові бази — компроміс. Appium 2.x з плагінною архітектурою помітно зручніший за першу версію. appium-doctor діагностує оточення — корисний при налаштуванні CI.
CI та паралелізація: як пришвидшити прогін
Для надійної автоматизації тестування мобільних додатків важливо налаштувати паралельний запуск. Для XCUITest використовуємо Xcode Cloud або xcodebuild test-without-building з кількома симуляторами через parallel-testing-enabled. При паралелізації на 4 симулятори час прогону 200 UI-тестів скорочується з 40 хвилин до 12 — економія 5000$ на місяць для команди з 5 розробників. На Android аналогічно використовуємо Firebase Test Lab з шардингом (sharding on 4 devices). Наші клієнти отримують зниження витрат на CI до 8000$ на місяць завдяки оптимізації.
| Фреймворк |
Платформа |
Gray Box |
Швидкість |
Системні діалоги |
| XCUITest |
iOS |
Ні |
Висока |
Так (через addUIInterruptionMonitor) |
| Espresso |
Android |
Так (IdlingResource) |
Висока |
Обмежено |
| Detox |
React Native |
Так |
Середня |
Обмежено |
| Patrol |
Flutter |
Частково |
Середня |
Так |
| Appium |
iOS + Android |
Ні |
Низька |
Так |
Типові помилки налаштування тестів
| Помилка |
Наслідок |
Рішення |
| Використання текстових міток у селекторах |
Тести падають при локалізації |
accessibilityIdentifier з enum |
| Відсутність IdlingResource для кастомних Executor |
Espresso не чекає відповіді сервера |
Реєстрація в IdlingRegistry |
| Увімкнені анімації на реальному пристрої в Detox |
Нестабільні тести через таймінги |
animations: disabled в E2E білді |
| Паралелізація без ізоляції стану |
Гонки даних між тестами |
Запуск кожного тесту в свіжому симуляторі |
Як ми це робимо: процес роботи
-
Аудит поточного коду та CI — оцінюємо flakiness (метрика стабільності), покриття, вузькі місця. Використовуємо Allure для збору звітів і Xcode Report для iOS.
-
Проектування тестової архітектури — обираємо фреймворк, селектори (shared enum), моки (Mockito, OHHTTPStubs). Визначаємо шари: unit → UI → E2E.
-
Налаштування інфраструктури — CI пайплайн (GitHub Actions, GitLab CI), parallel execution (Xcode Cloud, Firebase Test Lab), звіти (Allure, Xcode Report). Додаємо метрики: час прогону, кількість flaky-тестів.
-
Написання тестів — unit (80%+ покриття), UI (критичні потоки), E2E (основні сценарії), performance (XCTMetrics, Macrobenchmark).
-
Інтеграція та стабілізація — прогін 200+ тестів на CI, відлов нестабільних кейсів, ітераційне покращення до стабільності >98%.
-
Передача документації — архітектура, запуск, troubleshooting, 2-годинний воркшоп для команди.
Що входить в роботу (deliverables)
- Архітектурна документація тестового покриття
- Налаштований CI-пайплайн з паралелізацією та звітами (Allure, Xcode Report)
- Код тестів (unit, UI, E2E) з styleguide (наприклад, Google iOS Test Style)
- Навчання команди (2 години воркшопу)
- Доступ до тестових білдів та CI-логів
- Підтримка протягом місяця після здачі (фікс flakiness, оновлення під нові версії)
Терміни орієнтовно
Налаштування інфраструктури з нуля (CI, unit + UI тести, звіти) — 2–3 тижні. Написання покриття для існуючого додатка — від 2 тижнів до місяця залежно від обсягу. Оцінимо ваш проєкт за 2 дні — зв’яжіться з нами. 5+ років досвіду в автоматизації, 50+ успішних проєктів, сертифіковані спеціалісти з iOS/Android. Гарантуємо стабільність тестів >98% на CI після впровадження. Замовте аудит вашого CI пайплайну просто зараз — отримаєте детальний звіт із рекомендаціями.