Compose integrates with common testing frameworks.

## Interoperability with Espresso

In a hybrid app, you can find Compose components inside view hierarchies and
views inside Compose composables (via the [`AndroidView`](https://developer.android.com/reference/kotlin/androidx/compose/ui/viewinterop/AndroidView.composable.md.txt#AndroidView(kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1)) composable).

There are no special steps needed to match either type. You match views with
Espresso's [`onView`](https://developer.android.com/reference/androidx/test/espresso/Espresso.md.txt#onView(org.hamcrest.Matcher%3Candroid.view.View%3E)), and Compose elements with the [`ComposeTestRule`](https://developer.android.com/reference/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule.md.txt).

```kotlin
@Test
fun androidViewInteropTest() {
    // Check the initial state of a TextView that depends on a Compose state.
    Espresso.onView(withText("Hello Views")).check(matches(isDisplayed()))
    // Click on the Compose button that changes the state.
    composeTestRule.onNodeWithText("Click here").performClick()
    // Check the new value.
    Espresso.onView(withText("Hello Compose")).check(matches(isDisplayed()))
}
```

## Add View-scoped semantics for Compose interop testing

> [!NOTE]
> **Note:** The `onRootWithViewInteraction` API is included in [`androidx.compose.ui:ui-test-junit4:1.12.0-alpha01+`](https://developer.android.com/jetpack/androidx/releases/compose-ui.md.txt#1.12.0-alpha01) and [`androidx.compose.ui:ui-test:1.12.0-alpha01+`](https://developer.android.com/jetpack/androidx/releases/compose-ui.md.txt#1.12.0-alpha01). This API is experimental and subject to change.

### Scope Compose searches to specific Views

When migrating complex UIs to Compose, you might encounter identical Compose
elements nested inside multiple traditional Android Views---such as within a
`RecyclerView` or a `ViewPager`. In these scenarios, a standard Compose search
like `onNodeWithText("Save")` might fail with a "Multiple nodes found" error.

Instead of modifying your production code to inject dynamic test tags to
distinguish these elements, you can scope your Compose test directly to a
specific Android View.

Use the `onRootWithViewInteraction` API on your test rule. This function accepts
an Espresso `ViewInteraction`, allowing you to leverage Espresso to isolate a
specific container View and perform Compose interactions exclusively within
that scoped hierarchy.

> [!NOTE]
> **Note:** This API is specifically designed for testing Compose components nested inside Views. To test Views nested inside Compose, continue using standard Espresso interactions.

#### Interact with a list item

If you need to interact with a Compose element inside a specific `RecyclerView`
row, use Espresso to locate the row, then scope your Compose interaction to it.
This ignores identical Compose elements in all other rows.


```kotlin
@Test
fun testComposeButtonInsideRecyclerViewItem() = runComposeUiTest {
    // Scroll to the desired position using Espresso
    Espresso.onView(withId(recyclerViewId))
        .perform(RecyclerViewActions.scrollToPosition<MyViewHolder>(3))

    // Define an Espresso ViewInteraction that uniquely identifies the row
    val rowView = Espresso.onView(
        allOf(
            withId(rootViewId),
            hasDescendant(withText("Item #3"))
        )
    )

    // Scope the Compose search strictly to that specific row View
    onRootWithViewInteraction(rowView)
        .onNode(hasText("Like"))
        .performClick()
}
```

<br />

#### Resolve ambiguity in ViewPagers

When multiple fragments with identical Compose layouts are in memory
simultaneously, you can scope the search to the specific fragment's root View ID
to prevent matching ambiguity.


```kotlin
@Test
fun testComposeButtonInsideViewPagerItem() = runComposeUiTest {
    // Swipe to the desired page using Espresso
    Espresso.onView(withId(viewPagerViewId)).perform(swipeLeft())

    // Identify the specific container view using Espresso
    val fragmentB = Espresso.onView(withId(fragmentRootViewId))

    // The generic text "Save" is now unique within this view scope
    onRootWithViewInteraction(fragmentB)
        .onNode(hasText("Save"))
        .assertIsDisplayed()
}
```

<br />

## Interoperability with UiAutomator

By default, composables are accessible from [UiAutomator](https://developer.android.com/training/testing/other-components/ui-automator.md.txt) only by their
convenient descriptors (displayed text, content description, etc.). If you want
to access any composable that uses [`Modifier.testTag`](https://developer.android.com/reference/kotlin/androidx/compose/ui/Modifier.md.txt#(androidx.compose.ui.Modifier).testTag(kotlin.String)), you need to enable
the semantic property `testTagsAsResourceId` for the particular composable's
subtree. Enabling this behavior is useful for composables that don't have any
other unique handle, such as scrollable composables (for example, `LazyColumn`).

> [!NOTE]
> **Note:** This feature is available in Jetpack Compose version 1.2.0-alpha08 and higher.

Enable the semantic property only once high in your composables hierarchy to
ensure all nested composables with `Modifier.testTag` are accessible from
UiAutomator.

```kotlin
Scaffold(
    // Enables for all composables in the hierarchy.
    modifier = Modifier.semantics {
        testTagsAsResourceId = true
    }
){
    // Modifier.testTag is accessible from UiAutomator for composables nested here.
    LazyColumn(
        modifier = Modifier.testTag("myLazyColumn")
    ){
        // Content
    }
}
```

Any composable with the `Modifier.testTag(tag)` can be accessible with the use
of [`By.res(resourceName)`](https://developer.android.com/reference/androidx/test/uiautomator/BySelector.md.txt#res) using the same `tag` as the `resourceName`.

> [!CAUTION]
> **Caution:** Make sure you don't use [`By.res(resourcePackage, resourceId)`](https://developer.android.com/reference/androidx/test/uiautomator/BySelector.md.txt#res_1) as this formats the argument as `$resourcePackage:id/$resourceId`, which is different from `Modifier.testTag`.

```kotlin
val device = UiDevice.getInstance(getInstrumentation())

val lazyColumn: UiObject2 = device.findObject(By.res("myLazyColumn"))
// Some interaction with the lazyColumn.
```

## Additional Resources

- **[Test apps on Android](https://developer.android.com/training/testing.md.txt)**: The main Android testing landing page provides a broader view of testing fundamentals and techniques.
- **[Fundamentals of testing](https://developer.android.com/training/testing/fundamentals.md.txt):** Learn more about the core concepts behind testing an Android app.
- **[Local tests](https://developer.android.com/training/testing/local-tests.md.txt):** You can run some tests locally, on your own workstation.
- **[Instrumented tests](https://developer.android.com/training/testing/instrumented-tests.md.txt):** It is good practice to also run instrumented tests. That is, tests that run directly on-device.
- **[Continuous integration](https://developer.android.com/training/testing/continuous-integration.md.txt):** Continuous integration lets you integrate your tests into your deployment pipeline.
- **[Test different screen sizes](https://developer.android.com/training/testing/different-screens.md.txt):** With some many devices available to users, you should test for different screen sizes.
- **[Espresso](https://developer.android.com/training/testing/espresso.md.txt)**: While intended for View-based UIs, Espresso knowledge can still be helpful for some aspects of Compose testing.