Irbisa · Шпаргалка

Тестирование в Android

JUnit, MockK, Turbine, Robolectric, Espresso, тесты Compose UI, правила Hilt, скриншот-тесты

Middle-разработчик · 20 вопросов

  1. 01

    В чём разница между unit-, инструментальными и UI-тестами, и что должно лежать в src/test, а что в src/androidTest?

    Unit-тесты в src/test выполняются на JVM вашего компьютера за миллисекунды; инструментальные тесты из src/androidTest собираются в отдельный APK и запускаются на устройстве или эмуляторе, где есть настоящий фреймворк.

  2. 02

    Что такое тестовые дублёры (dummy, stub, fake, mock, spy) и когда fake лучше mock?

    Тестовый дублёр подменяет реальную зависимость в тесте; виды различаются объёмом поведения и тем, проверяет ли тест вызовы.

  3. 03

    Как правильно построить JUnit-тест: именование, Given/When/Then, проверки и @Before против @BeforeClass?

    Один тест проверяет одно поведение: подготовить входные данные, выполнить одно действие и проверить значения, чтобы ошибка показывала ожидаемое и фактическое.

    // wrong: two actions, boolean assertions, a name that describes the method@Test fun testCart() {  val cart = Cart()  cart.add(Item("apple", cents = 100))  cart.applyCoupon("HALF")  assertTrue(cart.totalCents == 50)   // fails with a bare AssertionError…
  4. 04

    Что такое TDD и какие правила тестирования вы бы ввели в Android-команде?

    TDD (разработка через тестирование) работает как цикл: написать падающий тест, написать минимум кода, чтобы он прошёл, затем отрефакторить под защитой теста (red, green, refactor).

  5. 05

    Почему JVM unit-тест падает с Method ... in android.util.Log not mocked, и как это исправить?

    В src/test android.jar содержит только сигнатуры методов, а тело каждого бросает `RuntimeException("Method ...

    // fails on the JVM: Log and Uri are stubs in src/testclass DeepLinkParser {  fun orderId(link: String): Long? {    Log.d("DeepLink", "parsing $link")     // RuntimeException: Method d ... not mocked    return Uri.parse(link).getQueryParameter("order")?.toLongOrNull()  }…
  6. 06

    Как в Espresso нажать на элемент RecyclerView, в том числе на тот, которого ещё нет на экране?

    Используйте RecyclerViewActions из espresso-contrib: actionOnItemAtPosition<VH>(3, click()) или actionOnItem<VH>(hasDescendant(withText("Order 42")), click()).

    // androidTestImplementation("androidx.test.espresso:espresso-contrib:<version>") // by positiononView(withId(R.id.orders))  .perform(RecyclerViewActions.actionOnItemAtPosition<OrderViewHolder>(3, click()))…
  7. 07

    Что такое UI Automator и когда его используют вместо Espresso?

    UI Automator управляет всем устройством через слой accessibility, поэтому может нажимать системные диалоги, уведомления, лаунчер и другие приложения; Espresso видит только view вашего приложения.

    @RunWith(AndroidJUnit4::class)class ScanFlowTest {  @get:Rule val activity = ActivityScenarioRule(MainActivity::class.java)  private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())   @Test fun scanAfterGrantingCamera() {…
  8. 08

    Mockito или MockK: как замокать final-классы и suspend-функции, и что делает relaxed = true?

    MockK создан для Kotlin: он из коробки мокает final-классы, object и функции-расширения, а suspend-функции стабит через coEvery и coVerify.

    class ArchiveUseCaseTest {  private val repo = mockk<OrderRepo>(relaxUnitFun = true)   // strict except Unit functions   @Test fun `archives the loaded order`() = runTest {    coEvery { repo.load(7) } returns Order(id = 7, archived = false)  // suspend: coEvery    val saved = slot<Order>()…
  9. 09

    Как протестировать ViewModel с корутинами: runTest, Dispatchers.setMain и StandardTestDispatcher против UnconfinedTestDispatcher?

    StandardTestDispatcher ставит корутины в очередь, пока тест не продвинет планировщик; UnconfinedTestDispatcher запускает их сразу на вызывающем потоке.

    class MainDispatcherRule(  val dispatcher: TestDispatcher = StandardTestDispatcher(),) : TestWatcher() {  override fun starting(description: Description) = Dispatchers.setMain(dispatcher)  override fun finished(description: Description) = Dispatchers.resetMain()}…
  10. 10

    Этот тест никогда не завершается и падает по таймауту. Почему, и как протестировать StateFlow правильно?

    @get:Rule val mainRule = MainDispatcherRule() @Test fun `search emits loading then results`() = runTest {  val vm = SearchViewModel(FakeRepo())   // state: StateFlow<UiState>   vm.search("room")  val states = vm.state.toList()   assertEquals(    listOf(UiState.Loading, UiState.Results(listOf("Room"))),    states,  )}

    toList() собирает значения до завершения flow, а StateFlow никогда не завершается, поэтому тест висит, пока runTest не сдастся (по умолчанию через 60 секунд).

  11. 11

    Найдите баг: этот тест ViewModel проходит локально, но на CI падает примерно в половине запусков.

    class ProfileViewModel(private val repo: ProfileRepo) : ViewModel() {  val state = MutableStateFlow<UiState>(UiState.Loading)   fun load() = viewModelScope.launch {    val name = withContext(Dispatchers.IO) { repo.profile() }    state.value = UiState.Loaded(name)  }} @Test fun `load shows the profile`() = runTest {   // MainDispatcherRule is set  val vm = ProfileViewModel(FakeProfileRepo())  vm.load()  advanceUntilIdle()  assertTrue(vm.state.value is UiState.Loaded)}

    withContext(Dispatchers.IO) уносит работу в настоящий пул потоков, которым тестовый планировщик не управляет, поэтому advanceUntilIdle() может вернуться раньше, чем придёт результат, и проверка соревнуется с IO-потоком.

  12. 12

    Найдите баг: этот тест остаётся зелёным, даже если удалить emit из pay().

    class CheckoutViewModel : ViewModel() {  private val _events = MutableSharedFlow<Event>()  val events: SharedFlow<Event> = _events  fun pay() = viewModelScope.launch { _events.emit(Event.OpenReceipt) }} @Test fun `pay opens the receipt`() = runTest {   // MainDispatcherRule is set  val vm = CheckoutViewModel()  backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {    vm.events.collect { assertEquals(Event.OpenReceipt, it) }  }  vm.pay()  advanceUntilIdle()}

    Проверка стоит внутри collect, поэтому выполняется, только если событие пришло: когда pay() ничего не отправляет, не срабатывает ни одна проверка, и тест проходит впустую.

  13. 13

    Как покрыть unit-тестом ViewModel, которая использует SavedStateHandle, берёт строки из Context или наблюдает за жизненным циклом?

    Соберите каждую часть сами: у SavedStateHandle(mapOf(...)) публичный конструктор, строки приходят через внедрённый интерфейс вместо Context, а TestLifecycleOwner позволяет тесту двигать жизненный цикл.

    // shown as an observer only to demonstrate TestLifecycleOwnerclass OrderViewModel(  savedState: SavedStateHandle,  private val repo: OrderRepository,) : ViewModel(), DefaultLifecycleObserver {  val orderId: Long = checkNotNull(savedState["orderId"])…
  14. 14

    Что такое Robolectric и чего он не может доказать по сравнению с инструментальным тестом?

    Robolectric выполняет настоящий код Android-фреймворка на JVM, заменяя нативные части и системные сервисы «тенями» (shadows), и тесты с Context, ресурсами или Activity работают в src/test за секунды.

    @RunWith(AndroidJUnit4::class)     // Robolectric, because the file is in src/test@Config(sdk = [35])class ReceiptFormatterTest {  private val context: Context = ApplicationProvider.getApplicationContext()   @Test fun formatsFromRealStringResources() {…
  15. 15

    Чего Espresso ждёт автоматически и когда нужен IdlingResource?

    Перед каждым perform и check Espresso ждёт, пока опустеет очередь сообщений главного потока и все зарегистрированные IdlingResource станут idle.

    // Ships in the app; the release build can use a no-op implementationobject AppIdling {  val network = CountingIdlingResource("network")} class SearchRepository(private val api: Api) {…
  16. 16

    Этот Compose-тест падает с «could not find any node», хотя цена видна на экране. Почему и как исправить?

    @Composablefun OrderRow(order: Order, onOpen: () -> Unit) {  Button(onClick = onOpen) {    Column {      Text(order.title)      Text(order.priceLabel, Modifier.testTag("price"))    }  }} @Test fun showsPrice() {  rule.setContent { OrderRow(Order(42, "Socks", "12.00 €"), onOpen = {}) }  rule.onNodeWithTag("price").assertTextEquals("12.00 €")}

    Finder-ы Compose по умолчанию ищут в объединённом (merged) дереве семантики, а Button сливает своих детей в один узел.

  17. 17

    Как протестировать воркер WorkManager и доказать, что его constraints действительно его блокируют?

    Логику воркера тестируйте напрямую через TestListenableWorkerBuilder и вызов doWork(); планирование проверяйте через WorkManagerTestInitHelper, SynchronousExecutor и TestDriver, который позволяет объявить ограничения (constraints) и задержки выполненными.

    @RunWith(AndroidJUnit4::class)class UploadWorkTest {  private val context: Context = ApplicationProvider.getApplicationContext()   @Before fun setUp() {    val config = Configuration.Builder()…
  18. 18

    Как работают screenshot-тесты на Android (Paparazzi, Roborazzi, на устройстве) и что держит golden-изображение стабильным?

    Screenshot-тест отрисовывает экран, попиксельно сравнивает его с утверждённым эталоном (golden) и падает при расхождении.

    // Paparazzi: layoutlib on the JVM, the device is a data classclass OrderCardTest {  @get:Rule val paparazzi = Paparazzi(    deviceConfig = DeviceConfig.PIXEL_5.copy(fontScale = 1.0f, locale = "en"),  )…
  19. 19

    Почему инструментальные UI-тесты бывают флаки и как найти и устранить причину?

    Чаще всего флаки-тесты возникают из четырёх источников: тайминг (анимации, sleep, несинхронизированная фоновая работа), реальные внешние зависимости (сеть, часы), состояние, общее для тестов, и незафиксированное устройство.

  20. 20

    Что на самом деле показывает покрытие кода и на чём бы вы построили порог для CI-сборки?

    Покрытие строк фиксирует, какие строки выполнились, а не какие поведения проверены, поэтому набор тестов без единой проверки может набрать 80%.