Testing


1. Testing Pyramid

Q: What is the testing pyramid and how does it apply to Android?

Layer Speed Scope Tools
Unit Tests Fast (ms) Single class/function JUnit, Mockito, Turbine
Integration Tests Medium Multiple components Robolectric, Room testing
UI Tests Slow (s) Full app/flows Espresso, Compose Testing

Aim for: Many unit tests, some integration tests, few UI tests.

Q: What should each layer test?

  • Unit: ViewModel logic, Repository, Use Cases, data transformations
  • Integration: Room DAOs, Repository with real database, navigation
  • UI: Critical user flows, screen state rendering, accessibility

2. Unit Testing

Q: How do you test ViewModels?

  1. Create ViewModel with fake dependencies
  2. Trigger actions
  3. Assert on exposed state

Key considerations:

  • Use TestDispatcher for coroutine control
  • Use runTest for test coroutine scope
  • Use Turbine for Flow testing

Q: How do you test coroutines?

Use kotlinx-coroutines-test:

  • runTest: Test coroutine scope with virtual time
  • StandardTestDispatcher: Queues coroutines, runs with advanceUntilIdle()
  • UnconfinedTestDispatcher: Runs coroutines eagerly

Inject TestDispatcher via constructor or Hilt.

Q: How do you test Flows?

Use Turbine library:

viewModel.state.test {
    assertEquals(Loading, awaitItem())
    assertEquals(Success(data), awaitItem())
    cancelAndIgnoreRemainingEvents()
}

Or use first(), take(), toList() for simpler cases.


3. Mocking

Q: When should you mock vs use fakes?

Fakes Mocks
Real implementation for testing Configured to return specific values
Easier to maintain Can verify interactions
Better for repositories, DAOs Better for third-party dependencies
More readable tests Tests coupled to implementation

Prefer fakes for your own code, mocks for things you don't control.

Q: What mocking libraries are common?

  • Mockito-Kotlin: Most popular, mature
  • MockK: Kotlin-first, coroutine support
  • Fake implementations: Hand-written, no framework

Q: What should you NOT mock?

  • Data classes / value objects
  • Your own simple classes (use real instances)
  • Android framework classes (use Robolectric or real device)

4. Testing Room

Q: How do you test Room DAOs?

Use in-memory database:

@Before
fun setup() {
    db = Room.inMemoryDatabaseBuilder(
        context, AppDatabase::class.java
    ).allowMainThreadQueries().build()
    dao = db.userDao()
}

@After
fun teardown() {
    db.close()
}

Test actual SQL queries—don't mock Room.

Q: What should DAO tests verify?

  • Insert and query return correct data
  • Update modifies existing records
  • Delete removes records
  • Queries filter correctly
  • Flow emits on data changes

5. UI Testing

Q: What's the difference between Espresso and Compose Testing?

Espresso Compose Testing
View-based UI Compose UI
Find by ID, text, content description Find by semantic properties
onView(withId(...)).perform(click()) onNodeWithText(...).performClick()

Q: How do you structure Compose UI tests?

@Test
fun showsLoadingThenContent() {
    composeTestRule.setContent {
        MyScreen(viewModel = fakeViewModel)
    }

    // Assert loading state
    composeTestRule.onNodeWithTag("loading").assertIsDisplayed()

    // Trigger loaded state
    fakeViewModel.setLoaded(data)

    // Assert content
    composeTestRule.onNodeWithText("Title").assertIsDisplayed()
}

Q: What makes UI tests reliable?

  • Use waitFor / waitUntil for async operations
  • Use test tags for stable identifiers (not user-facing text)
  • Inject controlled ViewModels/repositories
  • Disable animations in test device/emulator
  • Keep tests focused on one scenario

6. Testing Strategies

Q: What's the "Given-When-Then" pattern?

@Test
fun `user login with valid credentials succeeds`() {
    // Given: preconditions
    val fakeRepo = FakeAuthRepository(validCredentials = true)
    val viewModel = LoginViewModel(fakeRepo)

    // When: action
    viewModel.login("[email protected]", "password123")

    // Then: expected result
    assertEquals(LoginState.Success, viewModel.state.value)
}

Makes tests readable and self-documenting.

Q: How do you test error handling?

@Test
fun `network error shows error state`() = runTest {
    fakeRepository.setShouldThrowError(true)

    viewModel.loadData()

    val state = viewModel.state.value
    assertTrue(state is UiState.Error)
    assertEquals("Network error", (state as UiState.Error).message)
}

Test both happy path and error paths.

Q: What about testing navigation?

Options:

  1. Mock NavController, verify navigate() called
  2. Test with TestNavHostController
  3. For Compose: use NavHost in test, assert current destination

Keep navigation tests integration-level (not unit tests).


7. Test Doubles

Q: Explain different test doubles.

Type Purpose
Fake Working implementation with shortcuts (in-memory DB)
Stub Returns predetermined values
Mock Records calls, verifies interactions
Spy Wraps real object, tracks calls
Dummy Placeholder, never actually used

Q: Example of a Fake Repository?

class FakeUserRepository : UserRepository {
    private val users = mutableListOf<User>()

    override suspend fun getUsers(): List<User> = users

    override suspend fun addUser(user: User) {
        users.add(user)
    }

    fun clear() = users.clear()
    fun seed(list: List<User>) = users.addAll(list)
}

Simple, controllable, no mocking framework needed.


8. Testing Best Practices

Q: What makes a good unit test?

  • Fast: Milliseconds, not seconds
  • Independent: No order dependency
  • Repeatable: Same result every time
  • Self-validating: Pass or fail, no manual inspection
  • Timely: Written alongside production code

Q: What's code coverage and how much is enough?

Code coverage measures which code paths are exercised by tests. 70-80% is typical target. But:

  • 100% coverage doesn't mean good tests
  • Behavior coverage matters more than line coverage
  • Focus on critical paths and edge cases

Q: How do you test legacy code without tests?

  1. Write characterization tests (document current behavior)
  2. Identify seams for dependency injection
  3. Extract testable components
  4. Add tests before refactoring
  5. Refactor with test safety net

Quick Reference

Topic Key Points
Pyramid Many unit, some integration, few UI tests
Unit Tests Fast, isolated, test logic not frameworks
Coroutines runTest, TestDispatcher, Turbine for Flows
Mocking Prefer fakes for own code; mocks for third-party
Room In-memory database; test real SQL
UI Tests Compose Testing for Compose; Espresso for Views; disable animations
Best Practices FIRST principles; Given-When-Then; test behavior not implementation

results matching ""

    No results matching ""