Урок по треку Android 7 мин

Тестирование Android-приложения за 7 минут

Юнит-тесты ViewModel и корутин, моки и фейки, тесты Compose UI, Room и сети.

На собесе по тестированию проверяют практику: что вы тестируете, чем, как подменяете зависимости и как тестируете корутины и Flow. Теория пирамиды тестов нужна, но коротко.

Виды тестов

  • Локальные юнит-тесты (src/test): на JVM компьютера, без устройства, быстрые. ViewModel, use case, репозитории, мапперы.
  • Инструментальные тесты (src/androidTest): на устройстве или эмуляторе. UI, Room на настоящем SQLite, интеграция с Android API.
  • Robolectric: запускает часть Android-фреймворка на JVM, позволяет локально тестировать код, зависящий от Android (ресурсы, Context), и даже Compose UI. Быстрее инструментальных, но не настоящее устройство.
  • Скриншотные тесты (Paparazzi, Roborazzi, Compose Preview Screenshot Testing): сравнивают картинку экрана с эталоном.

Пирамида: много юнит-тестов, меньше интеграционных, немного сквозных UI-тестов. Чем выше, тем медленнее и хрупче.

Тест ViewModel с корутинами

class ProfileViewModelTest {
    @get:Rule val mainRule = MainDispatcherRule()   // Dispatchers.setMain(testDispatcher)

    @Test
    fun `показывает профиль после загрузки`() = runTest {
        val repo = FakeProfileRepository(profile = Profile("Аня"))
        val vm = ProfileViewModel(repo)

        vm.load()
        advanceUntilIdle()

        assertEquals(ProfileUiState.Content(Profile("Аня")), vm.state.value)
    }
}
  • runTest из kotlinx-coroutines-test запускает тест с виртуальным временем: delay(10_000) пролетает мгновенно.
  • Dispatchers.Main в JVM-тестах не существует, его подменяют Dispatchers.setMain(StandardTestDispatcher()) (обычно в JUnit-правиле) и сбрасывают resetMain().
  • StandardTestDispatcher ставит корутины в очередь, нужно advanceUntilIdle()/runCurrent(). UnconfinedTestDispatcher выполняет сразу, проще для простых тестов.
  • Диспетчеры IO/Default, захардкоженные внутри классов, мешают тестам: их лучше внедрять, чтобы подставить тестовый.

Тест Flow

Turbine делает проверку потоков читаемой:

@Test
fun `поиск выдаёт результаты`() = runTest {
    viewModel.results.test {
        assertEquals(emptyList<Item>(), awaitItem())
        viewModel.onQuery("кот")
        assertEquals(listOf(Item("кот")), awaitItem())
        cancelAndIgnoreRemainingEvents()
    }
}

Для StateFlow часто достаточно проверить value после advanceUntilIdle(). Если поток построен через stateIn(WhileSubscribed), без подписчика он не запустится: нужен коллектор в backgroundScope.

Фейки и моки

  • Фейк: упрощённая рабочая реализация интерфейса (FakeRepository со списком в памяти). Надёжен, проверяет поведение, переиспользуется. Google рекомендует фейки.
  • Мок (MockK, Mockito с mockito-kotlin): объект, у которого настраивают ответы (every { repo.load() } returns ...) и проверяют вызовы (verify). Удобен для проверки взаимодействий, но тесты становятся привязаны к реализации.

MockK понимает Kotlin: coEvery для suspend, final-классы, объекты. Мокать стоит границы (сеть, база), а не всё подряд, и не мокать data class.

Тесты Compose UI

@get:Rule val compose = createComposeRule()

@Test
fun showsError() {
    compose.setContent { ProfileScreen(state = ProfileUiState.Error, onRetry = {}) }
    compose.onNodeWithText("Повторить").assertIsDisplayed().performClick()
}

Поиск узлов по тексту, contentDescription или testTag, действия performClick, performTextInput, проверки assertIsDisplayed, assertTextEquals. Compose-тест сам ждёт простоя (idling). Stateless-экраны, принимающие UiState, тестировать легко: не нужна ViewModel.

Для View-системы есть Espresso (onView(withId(...)).perform(click())).

Room и сеть

  • Room: Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java) в инструментальном тесте или с Robolectric; проверка миграций MigrationTestHelper.
  • Сеть: MockWebServer из OkHttp отдаёт заранее заданные ответы, и Retrofit работает по-настоящему: проверяется сериализация, коды ошибок, заголовки.

Hilt в тестах

@HiltAndroidTest и HiltAndroidRule для инструментальных тестов, замена модулей через @TestInstallIn или @UninstallModules + @BindValue. В юнит-тестах ViewModel Hilt обычно не нужен: зависимости передают в конструктор.

Частые ошибки

  • Тесты без подмены Dispatchers.Main: IllegalStateException: Module with the Main dispatcher is missing.
  • Thread.sleep вместо виртуального времени.
  • Моки на всё, включая собственные data class.
  • Захардкоженные Dispatchers.IO внутри классов.
  • Проверка деталей реализации вместо поведения.
  • Только UI-тесты: медленно и хрупко.

Что спросят на собесе

  • Какие виды тестов есть в Android и что чем тестировать?
  • Как протестировать ViewModel с корутинами? Зачем setMain?
  • Чем фейк отличается от мока?
  • Как протестировать Flow?
  • Как писать тесты Compose UI?
  • Как протестировать сетевой слой и Room?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.