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?
- Create ViewModel with fake dependencies
- Trigger actions
- Assert on exposed state
Key considerations:
- Use
TestDispatcherfor coroutine control - Use
runTestfor test coroutine scope - Use
Turbinefor Flow testing
Q: How do you test coroutines?
Use kotlinx-coroutines-test:
runTest: Test coroutine scope with virtual timeStandardTestDispatcher: Queues coroutines, runs withadvanceUntilIdle()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/waitUntilfor 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("user@email.com", "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:
- Mock NavController, verify
navigate()called - Test with TestNavHostController
- For Compose: use
NavHostin 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?
- Write characterization tests (document current behavior)
- Identify seams for dependency injection
- Extract testable components
- Add tests before refactoring
- 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 |