Testing¶
Test Structure¶
Tests live in src/test/java within each module. Naming convention: <ClassName>Test.kt.
Dependencies: JUnit 4, MockK, Turbine, kotlinx-coroutines-test, shared:testing
ViewModel Test Pattern¶
class FeatureViewModelTest {
companion object {
private const val TEST_ID = "test-123"
private const val ERROR_MESSAGE = "Something went wrong"
}
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
private val useCase: FeatureUseCase = mockk()
private val errorMapper: ErrorMessageMapper = mockk()
private lateinit var viewModel: FeatureViewModel
@Before
fun setup() { setupDefaultMocks() }
private fun setupDefaultMocks() {
coEvery { errorMapper.map(any()) } returns ERROR_MESSAGE
}
private fun createViewModel() = FeatureViewModel(
useCase = useCase,
errorMapper = errorMapper,
dispatcherDefault = mainDispatcherRule.testDispatcher,
)
@Test
fun `initial state is Loading`() = runTest {
coEvery { useCase.getData() } returns Result.success(createData())
viewModel = createViewModel()
viewModel.uiState.test {
assertTrue(awaitItem() is FeatureUiState.Loading)
cancelAndIgnoreRemainingEvents()
}
}
}
Conventions¶
companion objectfor test constants at the top of the class@get:RuleforMainDispatcherRule(fromshared:testing)- Mocks declared as class properties,
@Beforefor common mock setup createViewModel()factory method for consistent initialization- No region comments — test names should be self-documenting
- Backtick names:
`action/condition expected result`
Common MockK Patterns¶
| Pattern | Usage |
|---|---|
coEvery { ... } |
Mock suspend functions |
every { ... } |
Mock regular functions |
coVerify { ... } |
Verify suspend function calls |
justRun { ... } |
Mock Unit-returning functions |
slot<T>() |
Capture arguments for verification |
awaitItem() |
Wait for next Flow emission (Turbine) |
cancelAndIgnoreRemainingEvents() |
End test without consuming remaining items |
Test Factory Functions¶
Factory functions create test data with sensible defaults. All parameters have empty/zero defaults (empty strings, 0, 0.0, emptyList(), etc.). Tests pass explicit values using constants from companion object — no inline magic values.
| Scope | Location | Consumed via |
|---|---|---|
| Cross-module | src/testFixtures/kotlin/.../testutil/ |
testImplementation(testFixtures(...)) |
| Module-local | src/test/java/.../testutil/ |
Direct import |
To enable testFixtures for a module, add to its build.gradle.kts:
UI Tests¶
Framework: Jetpack Compose UI Testing (androidx.compose.ui.test)
Location: src/androidTest/java/ within each module.
Run commands:
./gradlew connectedDebugAndroidTest # All modules
./gradlew :feature:session:connectedDebugAndroidTest # Per-module
Modules with UI tests: feature/session, feature/destinations, feature/settings
Screenshot Tests¶
Framework: Roborazzi (JVM-based, no emulator required)
Purpose: Visual regression testing for Compose components. Captures golden images of UI in known states and compares against them on subsequent runs.
Location: src/test/java/.../screenshot/ within each module. Golden images stored in
src/test/snapshots/.
Setup¶
Apply the convention plugin to enable screenshot testing in a module:
Add Robolectric SDK config:
Test Pattern¶
@RunWith(RobolectricTestRunner::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE)
internal class FeatureScreenshotTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun `state description`() {
composeTestRule.setContent {
TestTheme {
FeatureContent(state = createState())
}
}
composeTestRule.onRoot().captureRoboImage()
}
}
Key rules:
- Use
@RunWith(RobolectricTestRunner::class)+@GraphicsMode(GraphicsMode.Mode.NATIVE) - Wrap content in a
TestThemeusingCyclingLightColorScheme/CyclingDarkColorScheme - Test
Content-level composables with fixedUiState— no ViewModel, no live data - Use test factory functions for state creation (same pattern as unit test factories)
- Cover both light and dark themes for key states
Commands¶
./gradlew :module:recordRoborazziDebug # Record/update golden images
./gradlew :module:verifyRoborazziDebug # Verify against golden images (CI)
Workflow¶
- Write test → run
record→ golden PNG saved tosrc/test/snapshots/ - Commit golden images to git
- CI runs
verifyon PRs — fails if UI changed - If change is intentional → run
record→ commit updated goldens
Reference: SessionControlsOverlayScreenshotTest, GpxShareScreenshotTest, ScreenshotTestFactories.kt
Modules with screenshot tests: feature/session