Compose UI Tests

createComposeRule and Semantics-Based Assertions

A Compose 234 UI test never looks at pixels. It queries the semantics tree (Accessibility) that TalkBack reads. You need ui-test-junit4 for the rule and, as debugImplementation, ui-test-manifest, the empty activity createComposeRule() hosts content in. Since Compose 1.11 the rules live in a v2 package: the old ones ran coroutines eagerly on an UnconfinedTestDispatcher, while v2 queues them on a StandardTestDispatcher, as production code does.

src/test/.../ui/CartScreenTest.kt: find by text, assert, print the tree (excerpt)Kotlin
import androidx.compose.ui.test.junit4.v2.createComposeRule
...
@RunWith(AndroidJUnit4::class)
class CartScreenTest {
  @get:Rule val compose = createComposeRule()
  @Test fun showsCountTotalAndEveryBook() {
    compose.setContent { CartScreen(testBooks.take(2), onRemove = {}) }
    compose.onNodeWithText("Cart: 2 books, $38.99").assertIsDisplayed()
    compose.onNodeWithText("Out of stock").assertIsDisplayed()    // Salt and Saffron
    compose.onRoot().printToLog("CartScreen")                     // the semantics tree
  }

Every test uses three verbs: a finder (onNodeWithText, onNodeWithTag, onAllNodes(hasText(...))), an assertion (assertIsDisplayed, assertTextEquals, assertCountEquals) and an action (performClick, performTextInput, performTouchInput). Each waits for Compose to go idle first, so no sleep() is needed. When a finder fails, printToLog shows what is really there:

Output of 189
    D/CartScreen: printToLog:
    Printing with useUnmergedTree = 'false'
...
       CollectionInfo = 'CollectionInfo(rowCount=3, columnCount=1)'
...
        |-Node #25 at (l=16.0, t=16.0, r=37.0, b=52.0)px
        | Text = '[Cart: 2 books, $38.99]'

The list reports three rows (the heading and two cards), and the heading is only 21 pixels wide because Robolectric (Robolectric) has no real glyph widths, so assert on text and state, not sizes, in JVM tests. Where text isn't a stable handle, add Modifier.testTag("cart-total") and use onNodeWithTag.