This episode builds confidence in the UI: composable unit testing, UI tests with the Compose testing framework, semantics-based assertions and test tags, and integration testing from the UI to the data layer.

The UI is the part of an application that changes most often and breaks most easily. Episode 14 equips you with ways to test Compose so UI changes are no longer scary: from simple composable unit tests to integration tests that connect the UI with data.
The power of Compose testing lies in the Semantics API you learned in episode 13. Tests interact with semantics nodes — the same way TalkBack sees the application — so tests are more realistic than finding views by ID.
Episode 14 covers test setup, unit testing, semantics-based assertions, test tags, and integration testing.
Compose provides dedicated testing libraries. Add them in the Gradle build:
androidTestImplementation(platform("androidx.compose:compose-bom:2024.12.01"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-test-manifest")
testImplementation("junit:junit:4.13.2")ui-test-junit4 provides createComposeRule, and ui-test-manifest allows running UI tests in a debug build. There's no need to configure testOptions for animations — Compose tests disable animations automatically.
The createComposeRule rule provides a composition environment for tests. You can set content, perform interactions, and assert state:
@RunWith(AndroidJUnit4::class)
class PenghitungTest {
@get:Rule
val composeRule = createComposeRule()
@Test
fun tombolMenambahAngka() {
composeRule.setContent {
Penghitung()
}
composeRule.onNodeWithText("Ditekan 0 kali").assertIsDisplayed()
composeRule.onNodeWithText("Tambah").performClick()
composeRule.onNodeWithText("Ditekan 1 kali").assertIsDisplayed()
}
}onNodeWithText(...) finds a node by text, performClick() performs a click, and assertIsDisplayed() verifies the result. This test runs on an emulator or a device.
Compose tests provide many matchers: onNodeWithText, onNodeWithTag, onNodeWithContentDescription, and assertions like assertIsDisplayed, assertExists, assertTextEquals:
composeRule.onNodeWithText("Kirim").assertIsEnabled()
composeRule.onNodeWithContentDescription("Keranjang").assertExists()
composeRule.onNodeWithText("Pesan").assertIsSelected()onNodeWithContentDescription("Keranjang") uses the description from episode 13 — proof that accessibility and testing share the same semantics layer.
Sometimes text or descriptions change because of localization. Use test tags as stable anchors:
Card(
modifier = Modifier.testTag("kartu_produk_utama")
) {
Text("Produk Unggulan")
}composeRule.onNodeWithTag("kartu_produk_utama").assertIsDisplayed()Modifier.testTag("kartu_produk_utama") gives an identity that translations don't affect. onNodeWithTag uses that tag in tests — the ideal combination for long lists or dynamic text.
For a LazyColumn, nodes that haven't been rendered don't exist in the tree. Scroll first before asserting:
composeRule.onNodeWithTag("daftar_produk").performScrollToIndex(50)
composeRule.onNodeWithText("Produk 50").assertIsDisplayed()performScrollToIndex(50) scrolls the list to the index before the assertion. Lazy lists only render what is visible — the same principle you learned in episode 9.
Integration tests combine the real UI with real or fake data:
@Test
fun loginGagalMenampilkanError() {
composeRule.setContent {
LoginScreen(viewModel = fakeLoginViewModel())
}
composeRule.onNodeWithTag("field_email").performTextInput("salah")
composeRule.onNodeWithTag("field_password").performTextInput("123")
composeRule.onNodeWithText("Masuk").performClick()
composeRule.onNodeWithText("Email tidak valid").assertIsDisplayed()
}fakeLoginViewModel replaces the real ViewModel so the test focuses on the UI, not the network. This fake pattern keeps tests fast and deterministic.
Run all UI tests from the terminal:
./gradlew :app:connectedDebugAndroidTest./gradlew :app:connectedDebugAndroidTest runs the tests on a connected device or emulator. Results can be seen in the HTML and XML reports Gradle generates.
Episode 14 built confidence in the UI: test dependency setup, unit tests with createComposeRule, semantics-based assertions, test tags as stable anchors, lazy list testing, and integration tests with a fake ViewModel.
Key takeaways:
In episode 15 we will discuss performance optimization — minimizing recomposition, proper use of remember and derivedStateOf, stable keys, and UI performance benchmarking and debugging.