Compose UI tests fail at the dependency layer in three predictable ways: the JUnit4 surface is on the wrong source set, the ui-test-manifest artifact is missing or on the wrong configuration, or the host-test trio is incomplete. This skill encodes the exact matrix and the lint warning that polices it. Subsequent skills (choosing-test-rule-vs-runtest/SKILL.md, setting-up-host-vs-device-tests/SKILL.md) assume the dependencies are correct.
When to use this skill
- The user reports
java.lang.RuntimeException: Could not launch activity within 45 seconds. Examined activities: ... androidx.activity.ComponentActivityorActivityNotFoundException. - The IDE flags
createComposeRuleas unresolved orimport androidx.compose.ui.test.junit4.createComposeRuleas red. - Lint surfaces
TestManifestGradleConfiguration(severity WARNING) on abuild.gradle.ktsline. - The user wants to add Robolectric / host (JVM) tests for Compose and asks which artifacts to depend on.
- The user wants to opt into
enableAccessibilityChecks(...)and is unsure which artifact owns it.
When NOT to use this skill
- Dependencies are already correct and the user is choosing between
createComposeRule()andrunComposeUiTest { }— use./choosing-test-rule-vs-runtest/SKILL.md. - Dependencies compile but the test runs in the wrong source set (
test/vsandroidTest/) — use./setting-up-host-vs-device-tests/SKILL.md. - The test compiles and runs but is flaky/timing-out — start with
../../synchronization/synchronizing-with-idle/SKILL.md.
Prerequisites
- Android Gradle Plugin with the
com.android.applicationorcom.android.libraryplugin applied. - A Compose-enabled module (the
org.jetbrains.kotlin.plugin.composeplugin orbuildFeatures.compose = true). - A JUnit4 dependency on the test classpath (Compose's
ui-test-junit4artifact still uses JUnit4; JUnit5 requires a third-party Vintage engine bridge and is out of scope).
Workflow
-
1. Decide which test surfaces the module needs. A typical Compose feature module wants three: instrumentation tests for screenshot/RenderThread/full-stack coverage, host (Robolectric) tests for fast logic checks, and the manifest helper for
createComposeRule(). Pick from the matrix in step 2. -
2. Add dependencies on the correct configurations. Copy this into the module's
build.gradle.kts:
dependencies {
// --- Instrumentation tests (src/androidTest/) ---
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
// MUST be debugImplementation (see Pattern: WRONG vs RIGHT for ui-test-manifest below)
debugImplementation("androidx.compose.ui:ui-test-manifest")
// --- Host tests with Robolectric (src/test/) ---
testImplementation("androidx.compose.ui:ui-test") // common test API
testImplementation("androidx.compose.ui:ui-test-junit4") // ComposeTestRule + createComposeRule
testImplementation("androidx.compose.ui:ui-test-manifest") // ComponentActivity manifest entry
testImplementation("org.robolectric:robolectric:4.13") // pin a version
// --- Optional: accessibility validation (API 34+) ---
androidTestImplementation("androidx.compose.ui:ui-test-junit4-accessibility")
// Or, when using runComposeUiTest { } (no Rule):
androidTestImplementation("androidx.compose.ui:ui-test-accessibility")
}
The exact GA versions come from the Compose UI BOM or the developer's chosen composeVersion. Pin Robolectric independently — it does not ship in the Compose BOM.
- 3. If the test fails with
ActivityNotFoundExceptionforandroidx.activity.ComponentActivity, the manifest artifact is the cause 99 percent of the time. That artifact is a single-file library that merges this<activity>entry into the test APK manifest, taken fromcompose/ui/ui-test-manifest/src/main/AndroidManifest.xml:
<activity android:theme="@android:style/Theme.Material.Light.NoActionBar"
android:name="androidx.activity.ComponentActivity" android:exported="true" />
Without that entry, createComposeRule() and runComposeUiTest { } (which both default to launching androidx.activity.ComponentActivity via ActivityScenario.launch) crash at launch time. Diagnosis:
-
Confirm
ui-test-manifestis in the dependency list at all. -
Confirm it is on
debugImplementation(ortestImplementationfor host tests). If it is onandroidTestImplementation, lint flags it and Gradle still wires it in — but the AGP variant model intentionally separatesdebugandandroidTestmanifest merging, and putting it onandroidTestImplementationdoes NOT merge the<activity>into the host APK that the instrumentation runs against. The lint check exists precisely to catch this. -
4. Address the
TestManifestGradleConfigurationlint warning by moving the dependency todebugImplementation. The lint check lives incompose/ui/ui-test-manifest-lint/src/main/java/androidx/compose/ui/test/manifest/lint/GradleDebugConfigurationDetector.kt. Severity isSeverity.WARNINGand the quick fix replaces the configuration name withdebugImplementation. The check fires when the artifact appears onimplementation,api,compileOnly,runtimeOnly,androidTestImplementation,testImplementation, or any custom configuration whose name starts withandroid/test/ one of the supported config names. -
5. Decide whether the manifest artifact is needed at all. It is required when:
- The test uses
createComposeRule()(no type parameter — defaults toandroidx.activity.ComponentActivity). - The test uses
runComposeUiTest { }(no type parameter — same default).
It is NOT required when:
- The test uses
createAndroidComposeRule<MyActivity>()AND the developer declares<activity android:name=".MyActivity" .../>themselves insrc/androidTest/AndroidManifest.xml(orsrc/debug/AndroidManifest.xml). - The test uses
createEmptyComposeRule()and launches its ownActivityScenarioagainst an Activity already declared in the production manifest.
- The test uses
-
6. For host tests, ensure all three artifacts are on
testImplementation. Robolectric drives the JVM Looper, but Compose's idling/clock implementations requireui-test(common) plusui-test-junit4(ComposeTestRuleinterface andcreateComposeRuleactual). Withoutui-test-manifestontestImplementation, Robolectric throws the sameActivityNotFoundExceptionbecause it parses the same manifest. The host source set in androidx itself uses this configuration — seecompose/ui/ui-test/src/androidHostTest/kotlin/androidx/compose/ui/test/RobolectricComposeTest.kt. -
7. Add accessibility artifacts only if the developer plans to call
enableAccessibilityChecks. They are independent of the JUnit4 surface:ui-test-accessibilityextendsComposeUiTest(the receiver ofrunComposeUiTest { }).ui-test-junit4-accessibilityextendsComposeTestRule/AndroidComposeTestRule. Both are@RequiresApi(34), both are no-op on Robolectric (Build.FINGERPRINT == "robolectric"triggers a logged warning) — see the accessibility skill in this repo for follow-up.
-
8. Sync Gradle and re-run the test. A clean
./gradlew :app:connectedDebugAndroidTest(instrumentation) or./gradlew :app:testDebugUnitTest(host) should compile and locateComponentActivity.
Patterns
Pattern: WRONG vs RIGHT — ui-test-manifest configuration
// WRONG
dependencies {
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
androidTestImplementation("androidx.compose.ui:ui-test-manifest")
}
// WRONG because: lint emits TestManifestGradleConfiguration (WARNING) and the manifest
// merger does not pull the <activity> entry into the merged APK manifest from the
// androidTest source set. createComposeRule() crashes with ActivityNotFoundException.
// RIGHT
dependencies {
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-test-manifest")
}
The lint check explicitly allows debugImplementation and warns on every other configuration. From GradleDebugConfigurationDetector.kt:
"The androidx.compose.ui:ui-test-manifest dependency is needed for launching a
Compose host, such as with createComposeRule. However, it only needs to be present
in testing configurations therefore use this dependency with the debugImplementation
configuration"
Pattern: WRONG vs RIGHT — host-test dependencies
// WRONG
dependencies {
testImplementation("androidx.compose.ui:ui-test-junit4")
testImplementation("org.robolectric:robolectric:4.13")
// Missing ui-test-manifest -> ActivityNotFoundException at runComposeUiTest launch
// Missing ui-test -> linker errors on common test API symbols
}
// RIGHT
dependencies {
testImplementation("androidx.compose.ui:ui-test")
testImplementation("androidx.compose.ui:ui-test-junit4")
testImplementation("androidx.compose.ui:ui-test-manifest")
testImplementation("org.robolectric:robolectric:4.13")
}
Pattern: WRONG vs RIGHT — custom Activity without its own manifest entry
// WRONG
@get:Rule val rule = createAndroidComposeRule<MyHostActivity>()
// build.gradle.kts has only debugImplementation("androidx.compose.ui:ui-test-manifest")
// and src/androidTest/AndroidManifest.xml does NOT declare MyHostActivity.
// WRONG because: ui-test-manifest only declares ComponentActivity. Custom activities
// must be declared in the test APK manifest by the developer. Result: ActivityNotFoundException.
<!-- RIGHT — src/androidTest/AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity android:name=".MyHostActivity" android:exported="true" />
</application>
</manifest>
Pattern: complete sample build.gradle.kts
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
}
android {
namespace = "com.example.feature"
compileSdk = 35
defaultConfig {
minSdk = 23
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildFeatures { compose = true }
testOptions {
unitTests.isIncludeAndroidResources = true // required for Robolectric + Compose
}
}
dependencies {
val composeBom = platform("androidx.compose:compose-bom:2025.06.00")
implementation(composeBom)
androidTestImplementation(composeBom)
testImplementation(composeBom)
// Production
implementation("androidx.compose.ui:ui")
implementation("androidx.activity:activity-compose")
// Instrumentation
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-test-manifest")
androidTestImplementation("androidx.test.ext:junit:1.2.1")
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
// Host (Robolectric)
testImplementation("androidx.compose.ui:ui-test")
testImplementation("androidx.compose.ui:ui-test-junit4")
testImplementation("androidx.compose.ui:ui-test-manifest")
testImplementation("org.robolectric:robolectric:4.13")
testImplementation("junit:junit:4.13.2")
}
testOptions.unitTests.isIncludeAndroidResources = true is required so Robolectric can read merged resources during host tests of Compose content.
Mandatory rules
- MUST put
androidx.compose.ui:ui-test-manifestondebugImplementationfor instrumentation tests, and ontestImplementationfor host tests. Never onandroidTestImplementation,implementation, orapi. - MUST keep
ui-test-junit4on the same source set as the test that importscreateComposeRule/ComposeTestRule. MixingandroidTestImplementation(instrumentation) andtestImplementation(host) in the same module is fine; mixing them on the wrong test class is a compile error. - MUST declare any non-
ComponentActivitytest host (custom Activity) in the test APK manifest atsrc/androidTest/AndroidManifest.xmlorsrc/debug/AndroidManifest.xml. Theui-test-manifestartifact only declaresandroidx.activity.ComponentActivity. - MUST NOT remove the
TestManifestGradleConfigurationlint check vialintOptions { disable("TestManifestGradleConfiguration") }. Apply the quick fix instead — the check exists to prevent silent test failures. - MUST NOT put
ui-test-manifeston a release configuration (releaseImplementation,implementation). The artifact is purely a test scaffold and bloats production APKs with an unwanted<activity>entry. - PREFERRED: rely on the Compose BOM (
androidx.compose:compose-bom) so allui-test-*artifacts share a coherent version. Robolectric is independent and pins separately. - PREFERRED: when in doubt about which artifact owns a symbol, search
androidx/compose/ui/ui-test*paths from the corpus rather than guessing — see References.
Verification
-
./gradlew :<module>:connectedDebugAndroidTestcompiles and runs withoutActivityNotFoundExceptionforandroidx.activity.ComponentActivity. -
./gradlew :<module>:lintdoes NOT reportTestManifestGradleConfiguration. -
./gradlew :<module>:testDebugUnitTestcompiles when host tests userunComposeUiTest { }orcreateComposeRule(). - The dependency block visibly separates
androidTestImplementation(instrumentation),debugImplementation(manifest),testImplementation(host trio). - If the developer added accessibility artifacts,
enableAccessibilityChecks(...)resolves at compile time inside an instrumentation test. - The test APK manifest contains exactly one
<activity android:name="androidx.activity.ComponentActivity"/>entry (run./gradlew :app:processDebugAndroidTestManifestand inspectbuild/intermediates/.../AndroidManifest.xml).
References
- Compose testing setup (Android Developers): https://developer.android.com/develop/ui/compose/testing#setup
- Compose testing overview: https://developer.android.com/develop/ui/compose/testing
- Compose UI release notes: https://developer.android.com/jetpack/androidx/releases/compose-ui
- Compose Multiplatform testing: https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-test.html
compose/ui/ui-test-manifest-lint/src/main/java/androidx/compose/ui/test/manifest/lint/GradleDebugConfigurationDetector.kt—TestManifestGradleConfigurationissue definition (severity WARNING, quick-fix todebugImplementation).compose/ui/ui-test-manifest/src/main/AndroidManifest.xml— the merged<activity android:name="androidx.activity.ComponentActivity">entry thatcreateComposeRule()depends on.compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/ComposeUiTest.android.kt—runComposeUiTestlaunchesComponentActivity::class.javaby default (lines 184-197).compose/ui/ui-test/src/androidHostTest/kotlin/androidx/compose/ui/test/RobolectricComposeTest.kt— canonical host-test class skeleton with Robolectric (@RunWith(AndroidJUnit4::class) @Config(minSdk = RobolectricMinSdk)).compose/ui/ui-test/src/androidHostTest/kotlin/androidx/compose/ui/test/Constants.kt—internal const val RobolectricMinSdk = 23.