| name | compose-multiplatform-testing |
| description | Testing Compose Multiplatform UI — semantic assertions in commonTest, running UI tests on Android / Desktop / iOS, and screenshot regression strategies. Use when covering shared UI with tests. |
Compose Multiplatform Testing
Instructions
Compose MP 1.7+ ships a multiplatform UI-test API: the same composeTestRule you know from Android works in commonTest, driving semantic-tree assertions.
1. Dependencies
kotlin.sourceSets {
commonTest.dependencies {
implementation(kotlin("test"))
@OptIn(ExperimentalComposeLibrary::class)
implementation(compose.uiTest)
}
androidUnitTest.dependencies {
implementation(compose.uiTestJUnit4)
implementation("androidx.compose.ui:ui-test-manifest:1.7.0")
}
desktopTest.dependencies {
implementation(compose.desktop.uiTestJUnit4)
}
}
2. A shared UI test in commonTest
class CounterScreenTest {
@OptIn(ExperimentalTestApi::class)
@Test
fun incrementingUpdatesLabel() = runComposeUiTest {
val viewModel = CounterViewModel()
setContent { App(viewModel) }
onNodeWithText("Count: 0").assertIsDisplayed()
onNodeWithText("Increment").performClick()
onNodeWithText("Count: 1").assertIsDisplayed()
}
}
runComposeUiTest { … } replaces createComposeRule(); it's the multiplatform entry point. Runs on JVM-desktop by default; Android and iOS require their target source sets.
3. Semantic matchers
onNodeWithContentDescription("Remove from favorites").performClick()
onNodeWithTag("loginButton").assertIsEnabled()
onAllNodesWithText("Item", substring = true).assertCountEquals(3)
onNode(hasText("Save") and isEnabled()).performClick()
Prefer contentDescription and testTag over text — localized strings break text-based matchers.
4. Screenshot / pixel tests
There is no first-class shared screenshot API. Options by platform:
- Android (preferred for CI): Paparazzi renders Compose without a device. Works with Compose MP because the Android
composeTarget uses stock Jetpack Compose underneath.
- Desktop / JVM: Roborazzi or
compose-desktop's ImageComposeScene to dump PNGs.
- iOS: drive a real simulator from Xcode UI Tests against the Compose view controller; compare with
XCTAttachment.
5. Paparazzi sample
class CounterScreenshotTest {
@get:Rule val paparazzi = Paparazzi(
deviceConfig = DeviceConfig.PIXEL_5,
theme = "android:Theme.Material.Light.NoActionBar",
)
@Test
fun default() { paparazzi.snapshot { App(CounterViewModel()) } }
@Test
fun dark() {
paparazzi.snapshot {
CompositionLocalProvider(LocalInspectionMode provides true) {
MaterialTheme(colorScheme = darkColorScheme()) { App(CounterViewModel()) }
}
}
}
}
6. iOS UI tests via XCUITest
Your iosApp target gets a UI Test bundle in Xcode. Exercise the Compose screen via accessibility identifiers set on the Kotlin side:
Button(
onClick = ::increment,
modifier = Modifier.testTag("increment-btn").semantics { contentDescription = "increment-btn" },
) { Text("Increment") }
func testIncrement() {
let app = XCUIApplication(); app.launch()
app.buttons["increment-btn"].tap()
XCTAssert(app.staticTexts["Count: 1"].exists)
}
testTag maps to the accessibility identifier on iOS.
7. Animations & clock control
@OptIn(ExperimentalTestApi::class)
runComposeUiTest {
mainClock.autoAdvance = false
setContent { AnimatedCard(expanded = true) }
mainClock.advanceTimeBy(16)
onNodeWithTag("card").assertHeightIsAtLeast(120.dp)
}
Always disable autoAdvance for deterministic animation assertions — otherwise flakes appear in CI.
8. CI strategy
./gradlew :shared:desktopTest is the cheapest Compose-UI test runner — use for pure shared logic.
./gradlew :shared:testDebugUnitTest runs Paparazzi on Android.
- Real-device iOS UI tests run on macOS runners; keep them small and smoke-style.
Checklist