На собесе по тестированию проверяют практику: что вы тестируете, чем, как подменяете зависимости и как тестируете корутины и 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?