Getting tests written: unit and Compose UI
Writing tests is one of the areas where an agent helps most: the work is mechanical, the result is immediately checkable (the test passes or it doesn't), and it is the work people postpone most.
But there is a trap: the agent can write a test to make it pass rather than to verify behaviour. Typical symptoms:
- The test mirrors the code's implementation, so a code change forces a test change even when behaviour is unchanged.
- Weak assertions:
assertNotNull(result)doesn't check that the result is correct. - No error or boundary cases, only the happy path.
- So many mocks that the test really verifies the mocks, not the code.
// WEAK TEST — what the agent writes by default
@Test
fun testGetOrders() = runTest {
val repo = mockk<OrderRepository>()
coEvery { repo.getOrders() } returns listOf(mockk())
val vm = OrderViewModel(repo)
vm.load()
assertNotNull(vm.uiState.value) // what does this check? essentially nothing
}
// STRONG TEST — verifies behaviour
@Test
fun `load - ui state becomes error when the repository fails`() = runTest {
val repo = FakeOrderRepository(
result = Result.failure(IOException("no network"))
)
val vm = OrderViewModel(repo, testDispatcher)
vm.load()
val state = vm.uiState.value
assertTrue(state is OrderUiState.Error)
assertEquals(ErrorType.NETWORK, (state as OrderUiState.Error).type)
assertTrue(state.isRetryable)
}The difference is assertion strength. The second test describes what the code *should do*; the first only checks the call didn't crash.
| State in the task | What it prevents |
|---|---|
| "Cover error and empty cases too" | Happy-path-only tests |
| "Assertions must check the actual value, not just non-null" | Tests that verify nothing in practice |
| "Use fakes instead of mocks" | Tests that verify the mocks |
| "Bind tests to behaviour, not implementation" | Tests that break on every refactor |
| "Use `testTag` instead of text" (UI tests) | Tests that break when localisation changes |
The core APIs for Compose UI tests:
- `createComposeRule()` — when no Activity is needed.
- `createAndroidComposeRule<T>()` — when Activity access is required.
- Dependencies:
androidx.compose.ui:ui-test-junit4(androidTest) andandroidx.compose.ui:ui-test-manifest(debug). - Finders:
onNodeWithText,onNodeWithTag,onNode. - Actions and assertions:
performClick(),assertIsDisplayed().
The Compose test rule synchronises automatically — it waits until the UI is idle. So Thread.sleep is both unnecessary and harmful: it slows the test down and makes it brittle.
The strongest use of an agent for tests is writing tests retroactively for existing code. Untested legacy code makes refactoring frightening. Have the agent first write tests that pin down current behaviour (characterization tests), and the refactor becomes safe. This is one of the highest-value scenarios for an agent.
📚 Sources and documentation
- Testing your Compose layoutofficialdeveloper.android.com
`createComposeRule`, finders, assertions and dependencies.
- Test apps on Androidofficialdeveloper.android.com
- Test doubles (fakes, mocks)officialdeveloper.android.com
The official guidance on when a fake beats a mock.