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% рабочего времени на перезапуск и анализ ложнонегативных сбоев. Автоматизация без стабильности — не экономия, а потеря эффективности. Мы решаем эту проблему на уровне архитектуры: Gray Box-фреймворки (Detox, Patrol) синхронизируются с состоянием приложения, а native инструменты (XCUITest, Espresso) получают правильные IdlingResource и accessibilityIdentifier. Результат: стабильность >99% на CI.
Unit-тесты: что тестировать, а что нет
На iOS XCTest — основа. Бизнес-логика в ViewModel, Interactor, UseCase — тестируется без проблем, если она не тянет UIKit. Типичная ошибка: логика в UIViewController напрямую — тогда unit-тест требует создания view-иерархии, что медленно и нестабильно. Выход — выносить логику в сервисы с @testable import.
Для асинхронного кода в Swift: XCTestExpectation для старого стиля, await + XCTest async для современного. С Combine — XCTestExpectation + sink, но удобнее использовать библиотеки типа CombineExpectations. На Android JUnit 4/5 + Mockito для unit-тестов, Coroutines Test для suspend-функций. runTest {} из kotlinx-coroutines-test — стандарт для ViewModel с StateFlow. Покрытие кода unit-тестами на уровне 80% сокращает время регрессии на 60% (данные наших проектов).
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 на этапе аудита.
Detox и Patrol: end-to-end для React Native и Flutter
Detox — E2E фреймворк для React Native, разработанный Wix. Работает на реальных устройствах и симуляторах через Gray Box подход: знает о состоянии JS thread и синхронизируется с ним. Это решает главный flakiness-источник — тест не нажимает кнопку, пока приложение занято. Настройка 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. Время прогона 200 UI-тестов с параллелизацией на 4 симулятора — с 40 минут до 12. На Android аналогично используем Firebase Test Lab с шардингом.
| Фреймворк |
Платформа |
Gray Box |
Скорость |
Системные диалоги |
| XCUITest |
iOS |
Нет |
Высокая |
Да (через addUIInterruptionMonitor) |
| Espresso |
Android |
Да (IdlingResource) |
Высокая |
Ограничено |
| Detox |
React Native |
Да |
Средняя |
Ограничено |
| Patrol |
Flutter |
Частично |
Средняя |
Да |
| Appium |
iOS + Android |
Нет |
Низкая |
Да |
Типичные ошибки при настройке (и как их избежать)
| Ошибка |
Последствие |
Решение |
| Использование текстовых меток в селекторах |
Тесты падают при локализации |
accessibilityIdentifier из enum |
| Отсутствие IdlingResource для кастомных Executor |
Espresso не ждёт ответа сервера |
Регистрация в IdlingRegistry |
| Включённые анимации на реальном устройстве в Detox |
Flaky тесты из-за таймингов |
animations: disabled в E2E билде |
| Параллелизация без изоляции состояния |
Гонки данных между тестами |
Запуск каждого теста в свежем симуляторе |
Как мы это делаем: процесс работы
-
Аудит текущего кода и CI — оцениваем flakiness, покрытие, узкие места.
-
Проектирование тестовой архитектуры — выбираем фреймворк, селекторы, моки.
-
Настройка инфраструктуры — CI пайплайн, parallel execution, отчёты (Allure, Xcode Report).
-
Написание тестов — unit, UI, E2E, performance (XCTMetrics, Macrobenchmark).
-
Интеграция и стабилизация — прогон 200+ тестов, отлов flaky-кейсов.
-
Передача документации — архитектура, запуск, troubleshooting.
Что входит в работу (deliverables)
- Архитектурная документация тестового покрытия
- Настроенный CI-пайплайн с параллелизацией и отчётами
- Код тестов (unit, UI, E2E) с styleguide
- Обучение команды (2 часа воркшопа)
- Доступ к тестовым билдам и CI-логам
- Поддержка в течение месяца после сдачи (фикс flakiness, обновление под новые версии)
Сроки ориентировочно
Настройка инфраструктуры с нуля (CI, unit + UI тесты, отчёты) — 2-3 недели. Написание покрытия для существующего приложения — от 2 недель до месяца в зависимости от объёма. Оценим ваш проект за 2 дня — свяжитесь с нами. 5+ лет опыта в автоматизации, 50+ успешных проектов, сертифицированные специалисты по iOS/Android. Гарантируем стабильность тестов >98% на CI после внедрения.