Irbisa · Cheatsheet

Android Testing

JUnit, MockK, Turbine, Robolectric, Espresso, Compose UI tests, Hilt rules, screenshot tests

Middle Developer · 20 questions

  1. 01

    What is the difference between unit, instrumented and UI tests, and what belongs in src/test versus src/androidTest?

    Unit tests in src/test run on your computer's JVM in milliseconds; instrumented tests in src/androidTest are built into a separate APK and run on a device or emulator, where the real framework exists.

  2. 02

    What are test doubles (dummy, stub, fake, mock, spy), and when do you prefer a fake over a mock?

    A test double stands in for a real dependency in a test; the kinds differ in how much behaviour they have and whether the test checks how they were called.

  3. 03

    How do you structure a good JUnit test: naming, Given/When/Then, assertions, and @Before versus @BeforeClass?

    One behaviour per test: arrange the input, perform one action, and assert on values so the failure message shows expected and actual.

    // 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

    What is TDD, and what testing rules would you set for an Android team?

    TDD (test-driven development) is a loop: write a failing test, write the minimum code that makes it pass, then refactor with the test as a safety net (red, green, refactor).

  5. 05

    Why does a JVM unit test fail with Method ... in android.util.Log not mocked, and how do you fix it?

    In src/test the android.jar contains only method signatures, and every body throws `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

    How do you click an item in a RecyclerView with Espresso, including one that is not on screen yet?

    Use RecyclerViewActions from espresso-contrib: actionOnItemAtPosition<VH>(3, click()) or 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

    What is UI Automator, and when do you use it instead of Espresso?

    UI Automator drives the whole device through the accessibility layer, so it can tap system dialogs, notifications, the launcher and other apps; Espresso only sees your own app's views.

    @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 vs MockK: how do you mock final classes and suspend functions, and what does relaxed = true do?

    MockK is built for Kotlin: it mocks final classes, objects and extension functions out of the box and stubs suspend functions with coEvery and 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

    How do you unit-test a ViewModel with coroutines: runTest, Dispatchers.setMain, and StandardTestDispatcher versus UnconfinedTestDispatcher?

    StandardTestDispatcher queues coroutines until the test advances the scheduler; UnconfinedTestDispatcher starts them eagerly on the calling thread.

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

    This test never finishes and fails on a timeout. Why, and how would you test the StateFlow instead?

    @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() collects until the flow completes, and a StateFlow never completes, so the test suspends until runTest gives up (after 60 seconds by default).

  11. 11

    Find the bug: this ViewModel test passes locally but fails on CI about half the time.

    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) moves the work to a real thread pool the test scheduler does not control, so advanceUntilIdle() can return before the result is back and the assertion races the IO thread.

  12. 12

    Find the bug: this test still passes after someone deletes the emit from 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()}

    The assertion sits inside collect, so it runs only if an event arrives: when pay() emits nothing, no assertion executes and the test passes vacuously.

  13. 13

    How do you unit-test a ViewModel that uses SavedStateHandle, needs strings from a Context, or observes the lifecycle?

    Build each piece yourself: SavedStateHandle(mapOf(...)) is a public constructor, strings come through an injected interface instead of a Context, and TestLifecycleOwner lets the test move the lifecycle.

    // 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

    What is Robolectric, and what can it not prove compared with an instrumented test?

    Robolectric runs the real Android framework code on the JVM and replaces native and system-service parts with "shadows", so tests that need a Context, resources or an Activity run in src/test in seconds.

    @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

    What does Espresso wait for automatically, and when do you need an IdlingResource?

    Before every perform and check, Espresso waits until the main thread's message queue is idle and all registered IdlingResources are 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

    This Compose test fails with "could not find any node" although the price is visible on screen. Why, and how do you fix it?

    @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 €")}

    Compose finders search the merged semantics tree by default, and Button merges its children into one node.

  17. 17

    How do you test a WorkManager worker, and how do you prove its constraints actually gate it?

    Test the worker's logic directly with TestListenableWorkerBuilder and call doWork(); test the scheduling with WorkManagerTestInitHelper, a SynchronousExecutor and the TestDriver, which lets you declare constraints and delays as met.

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

    How do screenshot tests work on Android (Paparazzi, Roborazzi, on-device), and what keeps a golden image stable?

    A screenshot test renders a screen, compares it pixel by pixel with an approved "golden" image and fails on a difference.

    // 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

    What makes instrumented UI tests flaky, and how do you find and fix the cause?

    Most flakiness comes from four sources: timing (animations, sleeps, unsynchronised background work), real external dependencies (network, clock), state shared between tests, and an unpinned device.

  20. 20

    What does code coverage actually tell you, and what would you gate a CI build on?

    Line coverage records which lines ran, not which behaviours were checked, so a suite that asserts nothing can reach 80%.