Jetpack Compose Testing: Complete Guide (2026)
Jetpack Compose's test framework is a joy. Tree semantics, stable locators via semantics modifier, fast execution. If you are on Compose, you have better testing tools than most. This guide covers the
Jetpack Compose's test framework is a joy. Tree semantics, stable locators via semantics modifier, fast execution. If you are on Compose, you have better testing tools than most. This guide covers the patterns that make testing Compose apps productive.
The test pyramid
- Unit tests: state, business logic, ViewModel
- Composable tests: individual composables via
composeTestRule - Instrumented tests: multiple composables integrated
- UI tests (Espresso via Compose): full flows
Compose's composable tests run in the JVM via Robolectric, no device needed. Fastest UI-adjacent tests in Android history.
Composable tests
import androidx.compose.ui.test.*
class LoginScreenTest {
@get:Rule val composeTestRule = createComposeRule()
@Test
fun submit_button_calls_viewmodel() {
var submitted = false
composeTestRule.setContent {
LoginScreen(onSubmit = { _, _ -> submitted = true })
}
composeTestRule.onNodeWithTag("email_field").performTextInput("test@example.com")
composeTestRule.onNodeWithTag("password_field").performTextInput("pass")
composeTestRule.onNodeWithText("Sign In").performClick()
assertTrue(submitted)
}
}
Locators (semantics)
Semantics modifier makes composables testable:
TextField(
value = email, onValueChange = setEmail,
modifier = Modifier.testTag("email_field")
)
Use testTag for locators. Do not use onNodeWithText — text changes with i18n.
ViewModel tests
State hoisted out of composable → view model is testable in pure JVM:
class LoginViewModelTest {
@Test
fun `login with valid credentials transitions to authenticated`() = runTest {
val repo = FakeAuthRepo(success = true)
val vm = LoginViewModel(repo)
vm.submit("test@example.com", "correct")
advanceUntilIdle()
assertEquals(LoginState.Authenticated, vm.state.value)
}
}
Screenshot tests (Paparazzi, shot)
Paparazzi renders composables in JVM; compare to golden images.
class LoginScreenshotTest {
@get:Rule val paparazzi = Paparazzi()
@Test fun login_default() {
paparazzi.snapshot { LoginScreen(viewModel = previewViewModel()) }
}
@Test fun login_error() {
paparazzi.snapshot { LoginScreen(viewModel = previewViewModel(error = "Invalid")) }
}
}
Fast, deterministic, cross-platform.
UI tests (Espresso)
composeTestRule inside an Espresso test:
@HiltAndroidTest
class LoginFlowTest {
@get:Rule(order = 0) val hiltRule = HiltAndroidRule(this)
@get:Rule(order = 1) val composeTestRule = createAndroidComposeRule<MainActivity>()
@Test
fun successfulLogin() {
composeTestRule.onNodeWithTag("email").performTextInput("test@example.com")
composeTestRule.onNodeWithTag("password").performTextInput("correct")
composeTestRule.onNodeWithText("Sign In").performClick()
composeTestRule.onNodeWithText("Welcome").assertIsDisplayed()
}
}
Custom matchers
fun SemanticsNodeInteraction.assertIsEnabled() {
assert(isEnabled())
}
fun isEnabled(): SemanticsMatcher = SemanticsMatcher.expectValue(SemanticsProperties.Disabled, false)
Build domain-specific matchers for cleaner tests.
Async / coroutines
Compose tests auto-await idle state. For suspend functions in tests, use runTest:
@Test fun myTest() = runTest {
...
}
Advance dispatchers if needed:
advanceUntilIdle() // flushes pending coroutines
Accessibility in Compose
Semantics are accessibility:
Button(
onClick = onSubmit,
modifier = Modifier.semantics {
contentDescription = "Submit login form"
role = Role.Button
}
) { Text("Submit") }
Tests cover accessibility properties directly:
composeTestRule.onNodeWithContentDescription("Submit login form").assertIsDisplayed()
Run in CI
Unit + composable tests in Gradle task test. UI tests in connectedAndroidTest (requires emulator / device).
- run: ./gradlew test # fast
- run: ./gradlew connectedDebugAndroidTest # slower, gates release
How SUSA tests Compose apps
SUSA drives Compose apps via Android's accessibility layer. If composables have proper contentDescription / testTag, SUSA can find and interact. Generated Appium scripts use the same semantics.
susatest-agent test composeapp.apk --persona curious --steps 200
Best practices
- Test ViewModel state, not composable rendering. Fast, deterministic.
- Use testTag, not text. Stable across i18n.
- Paparazzi for visual regression. Screenshot tests without a device.
- Compose test rule for composable integration. Fast feedback.
- XCUITest-equivalent (Espresso + Compose) for critical flows only.
Compose's test framework is the best in any UI framework in 2026. Use it fully; ship confidently.
Test Your App Autonomously
Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.
Try SUSA Free