$30 off During Our Annual Pro Sale. View Details »

How to Test Your Compose UI (Kotlin Budapest Meetup)

How to Test Your Compose UI (Kotlin Budapest Meetup)

Jetpack Compose is the new and shiny framework that can finally replace XMLs and the View system in Android projects. While Compose is easy to adopt, not creating legacy code right at the start of such a journey requires some extra planning, awareness, and testing. In this talk, you'll have a quick look at how you can set up, write, and run Compose UI tests and how to write testable composables.

This talk was presented at the Kotlin Budapest Meetup, 2022.05.03.
https://www.meetup.com/Kotlin-Budapest/events/285101524/

István Juhos

May 03, 2022
Tweet

More Decks by István Juhos

Other Decks in Technology

Transcript

  1. stewemetal
    How to Test Your Compose UI
    István Juhos

    View Slide

  2. Kotlin Budapest Meetup – @stewemetal
    Source of examples
    https:!//github.com/stewemetal/composehydrationtracker

    View Slide

  3. Kotlin Budapest Meetup – @stewemetal
    Testing Views - Recap ☕
    • ViewGroup and View objects with rendering and
    behavior
    • Views are inflated from XML descriptors or
    instantiated in code
    • Views are directly referrable by View IDs

    View Slide

  4. Kotlin Budapest Meetup – @stewemetal
    Testing Views - Recap ☕
    android:id="@+id/entryList"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:layoutManager="LinearLayoutManager"
    tools:listitem="@layout/item_entry"
    tools:itemCount=”5"
    !/>

    android:id="@+id/amount"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_weight="1"
    tools:text="100 ml"
    !/>
    android:id="@+id/date"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    tools:text="2022-05-03"
    !/>
    !

    View Slide

  5. Kotlin Budapest Meetup – @stewemetal
    Testing Views - Recap ☕
    @RunWith(AndroidJUnit4!::class)
    class EntryListTest {
    @get:Rule
    var activityScenarioRule = activityScenarioRule()
    @Test
    fun testList() {
    onView(withId(R.id.entryList))
    .perform(!!...)
    }
    }

    View Slide

  6. Kotlin Budapest Meetup – @stewemetal
    Testing Views - Recap ☕
    @RunWith(AndroidJUnit4!::class)
    class EntryListTest {
    @get:Rule
    var activityScenarioRule = activityScenarioRule()
    @Test
    fun testList() {
    onView(withId(R.id.entryList))
    .perform(!!...)
    }
    }

    View Slide

  7. Kotlin Budapest Meetup – @stewemetal
    Testing Compose UI
    • No direct access to the Composition
    • No UI objects to look up
    • No IDs
    • Not every Composable emits UI

    View Slide

  8. Kotlin Budapest Meetup – @stewemetal
    Testing Compose UI
    🧪

    View Slide

  9. Kotlin Budapest Meetup – @stewemetal
    Dependencies
    androidTestImplementation "androidx.compose.ui:ui-test:$compose_version"
    !// Test rules and transitive dependencies:
    androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
    !// Needed for createComposeRule, but not createAndroidComposeRule:
    debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"

    View Slide

  10. Kotlin Budapest Meetup – @stewemetal
    @Composable
    fun EntryList(
    entries: List = emptyList(),
    ) {
    LazyColumn {
    items(entries) { item ->
    HydrationItem(item = item)
    }
    }
    }
    @Composable
    fun HydrationItem(item: HydrationEntry) {
    Row(
    modifier = Modifier
    .wrapContentHeight(
    align = Alignment.CenterVertically,
    Composables to test: EntryList

    View Slide

  11. Kotlin Budapest Meetup – @stewemetal
    HydrationItem(item = item)
    }
    }
    }
    @Composable
    fun HydrationItem(item: HydrationEntry) {
    Row(
    modifier = Modifier
    .wrapContentHeight(
    align = Alignment.CenterVertically,
    )
    .padding(16.dp)
    ) {
    Text(
    "${item.milliliters} ml",
    modifier = Modifier.weight(1f),
    )
    Text("${item.dateTime}")
    }
    }
    Composables to test: HydrationItem

    View Slide

  12. Kotlin Budapest Meetup – @stewemetal
    The anatomy of Compose UI tests
    @RunWith(AndroidJUnit4!::class)
    class EntryListTest {
    @get:Rule
    val composeTestRule =
    !// !!...
    }
    createComposeRule()

    View Slide

  13. Kotlin Budapest Meetup – @stewemetal
    The anatomy of Compose UI tests
    @RunWith(AndroidJUnit4!::class)
    class EntryListTest {
    @get:Rule
    val composeTestRule =
    !// !!...
    }
    createComposeRule()

    View Slide

  14. Kotlin Budapest Meetup – @stewemetal
    The anatomy of Compose UI tests
    @RunWith(AndroidJUnit4!::class)
    class EntryListTest {
    @get:Rule
    val composeTestRule =
    !// ComposeContentTestRule
    !// !!...
    }
    createComposeRule()

    View Slide

  15. Kotlin Budapest Meetup – @stewemetal
    The anatomy of Compose UI tests
    @RunWith(AndroidJUnit4!::class)
    class EntryListTest {
    @get:Rule
    val composeTestRule =
    !// !!...
    }
    create ComposeRule ()
    Android

    View Slide

  16. Kotlin Budapest Meetup – @stewemetal
    The anatomy of Compose UI tests
    @RunWith(AndroidJUnit4!::class)
    class EntryListTest {
    @get:Rule
    val composeTestRule =
    !// !!...
    }
    create ComposeRule
    Android ()

    View Slide

  17. Kotlin Budapest Meetup – @stewemetal
    The anatomy of Compose UI tests
    @RunWith(AndroidJUnit4!::class)
    class EntryListTest {
    @get:Rule
    val composeTestRule =
    !// !!...
    }
    createComposeRule()

    View Slide

  18. Kotlin Budapest Meetup – @stewemetal
    @get:Rule val composeTestRule = createComposeRule()
    @Test
    fun entriesExist_entriesDisplayed() {
    composeTestRule.apply {
    setContent {
    HydrationTrackerTheme {
    EntryList(
    entries = entries
    )
    }
    }
    !// !!...
    }
    }
    The anatomy of Compose UI tests

    View Slide

  19. Kotlin Budapest Meetup – @stewemetal
    The anatomy of Compose UI tests
    @get:Rule val composeTestRule = createComposeRule()
    @Test
    fun entriesExist_entriesDisplayed() {
    composeTestRule.apply {
    setContent {
    HydrationTrackerTheme {
    EntryList(
    entries = entries
    )
    }
    }
    !// !!...
    }
    }

    View Slide

  20. Kotlin Budapest Meetup – @stewemetal
    The anatomy of Compose UI tests
    @get:Rule val composeTestRule = createComposeRule()
    @Test
    fun entriesExist_entriesDisplayed() {
    composeTestRule.apply {
    setContent {
    HydrationTrackerTheme {
    EntryList(
    entries = entries
    )
    }
    }
    !// !!...
    }
    }

    View Slide

  21. Kotlin Budapest Meetup – @stewemetal
    The anatomy of Compose UI tests
    @get:Rule val composeTestRule = createComposeRule()
    @Test
    fun entriesExist_entriesDisplayed() {
    composeTestRule.apply {
    setContent {
    HydrationTrackerTheme {
    EntryList(
    entries = entries
    )
    }
    }
    !// !!...
    }
    }

    View Slide

  22. Kotlin Budapest Meetup – @stewemetal
    The anatomy of Compose UI tests
    @get:Rule val composeTestRule = createComposeRule()
    @Test
    fun entriesExist_entriesDisplayed() {
    composeTestRule.apply {
    setContent {
    HydrationTrackerTheme {
    EntryList(
    entries = entries
    )
    }
    }
    !// !!...
    }
    }

    View Slide

  23. Kotlin Budapest Meetup – @stewemetal
    The anatomy of Compose UI tests
    @get:Rule val composeTestRule = createComposeRule()
    @Test
    fun entriesExist_entriesDisplayed() {
    composeTestRule.apply {
    setContent {
    HydrationTrackerTheme {
    EntryList(
    entries = entries
    )
    }
    }
    !// !!...
    }
    }
    private val entries = listOf(
    HydrationEntry(…),
    HydrationEntry(…),
    HydrationEntry(…),
    )

    View Slide

  24. Kotlin Budapest Meetup – @stewemetal
    The anatomy of Compose UI tests
    @get:Rule val composeTestRule = createComposeRule()
    @Test
    fun entriesExist_entriesDisplayed() {
    composeTestRule.apply {
    setContent {
    HydrationTrackerTheme {
    EntryList(
    entries = entries
    )
    }
    }
    !// !!...
    }
    }
    So, what can we do here without view IDs? 🤔

    View Slide

  25. Kotlin Budapest Meetup – @stewemetal
    The semantics tree 🌳

    View Slide

  26. Kotlin Budapest Meetup – @stewemetal
    The semantics tree 🌳
    • A parallel tree next to the Composition 🌲🌳
    • Used for accessibility and testing
    • Composables (can) contribute to the semantics tree

    View Slide

  27. Kotlin Budapest Meetup – @stewemetal
    The semantics tree 🌳
    composeTestRule.apply {
    setContent {
    Text("100 ml")
    }
    }
    Semantics tree
    root node
    Semantic node with text
    "100 ml"

    View Slide

  28. Kotlin Budapest Meetup – @stewemetal
    The semantics tree 🌳
    composeTestRule.apply {
    setContent {
    Text("100 ml")
    }
    }
    Root
    Text = '[100 ml]'

    View Slide

  29. Kotlin Budapest Meetup – @stewemetal
    The semantics tree 🌳
    composeTestRule.apply {
    setContent {
    Row {
    Text("100 ml")
    }
    }
    }
    Root
    Text = '[100 ml]'

    View Slide

  30. Kotlin Budapest Meetup – @stewemetal
    The semantics tree 🌳
    composeTestRule.apply {
    setContent {
    Row {
    Text("100 ml")
    Text("2022-05-03")
    }
    }
    }
    Text = '[100 ml]' Text = '[2022-05-03]'
    Root

    View Slide

  31. Kotlin Budapest Meetup – @stewemetal
    The semantics tree 🌳
    • We can affect how the semantics tree is built
    Row(
    modifier = Modifier
    .semantics {
    !// !!...
    }
    ) {
    Text("100 ml")
    Text("2022-05-03")
    }

    View Slide

  32. Kotlin Budapest Meetup – @stewemetal
    The semantics tree 🌳
    • We can affect how the semantics tree is built
    Row(
    modifier = Modifier
    .semantics {
    !// !!...
    }
    ) {
    Text("100 ml")
    Text("2022-05-03")
    }

    View Slide

  33. Kotlin Budapest Meetup – @stewemetal
    The semantics tree 🌳
    • We can affect how the semantics tree is built
    Row(
    modifier = Modifier
    .semantics {
    !// !!...
    }
    ) {
    Text("100 ml")
    Text("2022-05-03")
    }
    Root
    Text = '[100 ml]' Text = '[2022-05-03]'

    View Slide

  34. Kotlin Budapest Meetup – @stewemetal
    The semantics tree 🌳
    • We can affect how the semantics tree is built
    Row(
    modifier = Modifier
    .semantics {
    contentDescription = "List item"
    }
    ) {
    Text("100 ml")
    Text("2022-05-03")
    }
    Root
    Text = '[100 ml]' Text = '[2022-05-03]'
    ContentDescription =
    '[List item]'

    View Slide

  35. Kotlin Budapest Meetup – @stewemetal
    The semantics tree 🌳
    • We can affect how the semantics tree is built
    Row(
    modifier = Modifier
    .semantics {
    testTag = "List item"
    }
    ) {
    Text("100 ml")
    Text("2022-05-03")
    }
    Root
    Text = '[100 ml]' Text = '[2022-05-03]'
    Tag =
    '[List item]'

    View Slide

  36. Kotlin Budapest Meetup – @stewemetal
    Visualizing the semantics tree 🌳👀
    Text = '[100 ml]' Text = '[2022-05-03]'
    Root
    Row {
    Text("100 ml")
    Text("2022-05-03")
    }

    View Slide

  37. Kotlin Budapest Meetup – @stewemetal
    Visualizing the semantics tree 🌳👀
    • In Compose tests
    • printToLog("TAG")
    Text = '[100 ml]' Text = '[2022-05-03]'
    Root

    View Slide

  38. Kotlin Budapest Meetup – @stewemetal
    Visualizing the semantics tree 🌳👀
    • In Compose tests
    onRoot()
    .printToLog("RowAndTexts") Text = '[100 ml]' Text = '[2022-05-03]'
    Root

    View Slide

  39. Kotlin Budapest Meetup – @stewemetal
    Visualizing the semantics tree 🌳👀
    • In Compose tests
    onRoot()
    .printToLog("RowAndTexts")
    D/RowAndTexts: printToLog:
    Printing with useUnmergedTree = 'false'
    Node #1 at (l=0.0, t=66.0, r=224.0, b=125.0)px
    |-Node #2 at (l=0.0, t=66.0, r=136.0, b=125.0)px
    | Text = '[100 ml]'
    | Actions = [GetTextLayoutResult]
    |-Node #3 at (l=0.0, t=66.0, r=224.0, b=125.0)px
    Text = '[2022-05-03]'
    Actions = [GetTextLayoutResult]
    Text = '[100 ml]' Text = '[2022-05-03]'
    Root

    View Slide

  40. Kotlin Budapest Meetup – @stewemetal
    Visualizing the semantics tree 🌳👀
    • In Compose tests
    • printToLog("TAG")
    onRoot()
    .printToLog("RowAndTexts")
    D/RowAndTexts: printToLog:
    Printing with useUnmergedTree = 'false'
    Node #1 at (l=0.0, t=66.0, r=224.0, b=125.0)px
    |-Node #2 at (l=0.0, t=66.0, r=136.0, b=125.0)px
    | Text = '[100 ml]'
    | Actions = [GetTextLayoutResult]
    |-Node #3 at (l=0.0, t=66.0, r=224.0, b=125.0)px
    Text = '[2022-05-03]'
    Actions = [GetTextLayoutResult]
    Text = '[100 ml]' Text = '[2022-05-03]'
    Root

    View Slide

  41. Kotlin Budapest Meetup – @stewemetal
    Visualizing the semantics tree 🌳👀
    • In Compose tests
    • printToLog("TAG")
    onRoot()
    .printToLog("RowAndTexts")
    D/RowAndTexts: printToLog:
    Printing with useUnmergedTree = 'false'
    Node #1 at (l=0.0, t=66.0, r=224.0, b=125.0)px
    |-Node #2 at (l=0.0, t=66.0, r=136.0, b=125.0)px
    | Text = '[100 ml]'
    | Actions = [GetTextLayoutResult]
    |-Node #3 at (l=0.0, t=66.0, r=224.0, b=125.0)px
    Text = '[2022-05-03]'
    Actions = [GetTextLayoutResult]
    Text = '[100 ml]' Text = '[2022-05-03]'
    Root

    View Slide

  42. Kotlin Budapest Meetup – @stewemetal
    Visualizing the semantics tree 🌳👀
    • In Compose tests
    • printToLog("TAG")
    onRoot()
    .printToLog("RowAndTexts")
    D/RowAndTexts: printToLog:
    Printing with useUnmergedTree = 'false'
    Node #1 at (l=0.0, t=66.0, r=224.0, b=125.0)px
    |-Node #2 at (l=0.0, t=66.0, r=136.0, b=125.0)px
    | Text = '[100 ml]'
    | Actions = [GetTextLayoutResult]
    |-Node #3 at (l=0.0, t=66.0, r=224.0, b=125.0)px
    Text = '[2022-05-03]'
    Actions = [GetTextLayoutResult]
    Text = '[100 ml]' Text = '[2022-05-03]'
    Root

    View Slide

  43. Kotlin Budapest Meetup – @stewemetal
    Visualizing the semantics tree 🌳👀
    • In Android Studio
    • Layout Inspector

    View Slide

  44. Kotlin Budapest Meetup – @stewemetal
    Merging semantics
    Row(
    modifier = Modifier
    .wrapContentHeight(
    align Alignment.CenterVertically,
    )
    .padding(16.dp)
    ) {
    Text(
    "${item.milliliters} ml",
    modifier = Modifier.weight(1f),
    )
    Text(
    "${item.dateTime}",
    )
    }
    Text = '[2022-05-03]'
    Text = '[100 ml]'
    Root
    =

    View Slide

  45. Kotlin Budapest Meetup – @stewemetal
    Merging semantics
    Row(
    modifier = Modifier
    .wrapContentHeight(
    align Alignment.CenterVertically,
    )
    .padding(16.dp)
    .semantics(mergeDescendants = false) {}
    ) {
    Text(
    "${item.milliliters} ml",
    modifier = Modifier.weight(1f),
    )
    Text(
    "${item.dateTime}",
    )
    }
    =
    Text = '[2022-05-03]'
    Text = '[100 ml]'
    Root

    View Slide

  46. Kotlin Budapest Meetup – @stewemetal
    Merging semantics
    Text = '[100 ml, 2022-05-01]'
    Root
    Row(
    modifier = Modifier
    .wrapContentHeight(
    align Alignment.CenterVertically,
    )
    .padding(16.dp)
    .semantics(mergeDescendants = true) {}
    ) {
    Text(
    "${item.milliliters} ml",
    modifier = Modifier.weight(1f),
    )
    Text(
    "${item.dateTime}",
    )
    }
    =

    View Slide

  47. Kotlin Budapest Meetup – @stewemetal
    Merged semantics by default
    Button(
    onClick = {},
    ) {
    Text("100 ml")
    }
    Role = 'Button'
    Focused = 'false'
    Text = '[100 ml]'
    Actions = [OnClick, GetTextLayoutResult]
    MergeDescendants = 'true'

    View Slide

  48. Kotlin Budapest Meetup – @stewemetal
    Using the semantics tree!!...
    @get:Rule val composeTestRule = createComposeRule()
    @Test
    fun entriesExist_entriesDisplayed() {
    composeTestRule.apply {
    setContent {
    HydrationTrackerTheme {
    EntryList(
    entries = entries
    )
    }
    }
    !// !!...
    }
    }
    !!...we can test Compose UI 🤩

    View Slide

  49. Kotlin Budapest Meetup – @stewemetal
    Select nodes - Finders
    • onRoot
    onRoot()
    .printToLog(”TAG") Root
    Text = '[100 ml, 2022-05-03]'
    Tag = '[List item]'
    ContentDescription =
    '[100 ml on 2022-05-03]'

    View Slide

  50. Kotlin Budapest Meetup – @stewemetal
    Select nodes - Finders
    • onNodeWithTag
    onNodeWithTag("List item")
    Root
    Text = '[100 ml, 2022-05-03]'
    Tag = '[List item]'
    ContentDescription =
    '[100 ml on 2022-05-03]'

    View Slide

  51. Kotlin Budapest Meetup – @stewemetal
    Select nodes - Finders
    • onNodeWithContentDescription
    onNodeWithContentDescription(
    "100 ml on 2022-05-03",
    )
    Root
    Text = '[100 ml, 2022-05-03]'
    Tag = '[List item]'
    ContentDescription =
    '[100 ml on 2022-05-03]'

    View Slide

  52. Kotlin Budapest Meetup – @stewemetal
    Select nodes - Finders
    • onNodeWithText
    onNodeWithText(
    "100 ml",
    )
    Root
    Text = '[100 ml, 2022-05-03]'
    Tag = '[List item]'
    ContentDescription =
    '[100 ml on 2022-05-03]'

    View Slide

  53. Kotlin Budapest Meetup – @stewemetal
    Select nodes - Finders
    • onNodeWithText
    onNodeWithText(
    "100 ml",
    useUnmergedTree = false,
    )
    Root
    Text = '[100 ml, 2022-05-03]'
    Tag = '[List item]'
    ContentDescription =
    '[100 ml on 2022-05-03]'

    View Slide

  54. Kotlin Budapest Meetup – @stewemetal
    Select nodes - Finders
    • onNodeWithText
    Root
    Text = '[100 ml]' Text = '[2022-05-03]'
    Tag = '[List item]'
    ContentDescription =
    '[100 ml on 2022-05-03]'
    onNodeWithText(
    "100 ml",
    useUnmergedTree = true,
    )

    View Slide

  55. Kotlin Budapest Meetup – @stewemetal
    Select nodes - Finders
    • onAllNodesWith!!...
    Tag = '[entries_list]'
    onAllNodesNodeWithTag("List item")
    Tag = '[List item]'
    Text = '[500 ml, 2022-05-03]'
    Tag = '[List item]'
    Text = '[100 ml, 2022-04-30]'
    !!...

    View Slide

  56. Kotlin Budapest Meetup – @stewemetal
    Actions
    • performClick
    • performScrollTo!!...
    • performTextInput
    • performGesture
    • performKeyPress
    • !!...
    onNodeWithText("100 ml")
    .performClick()

    View Slide

  57. Kotlin Budapest Meetup – @stewemetal
    Assertions
    • assertExists
    • assertIsDisplayed
    • assertIsEnabled
    • assertContentDescription
    • assertTextEquals
    • assert() with matchers
    • !!...
    onNodeWithText("100 ml")
    .assertIsEnabled()

    View Slide

  58. Kotlin Budapest Meetup – @stewemetal
    Testing in isolation
    • Structure your composables with testing in mind
    @Composable
    fun EntryList(
    entries: List = emptyList()
    ) {
    LazyColumn(
    modifier = Modifier.testTag("entries_list")
    ) {
    items(entries) { item ->
    HydrationItem(item = item)
    }
    }
    }

    View Slide

  59. Kotlin Budapest Meetup – @stewemetal
    Testing in isolation
    • Structure your composables with testing in mind
    @Composable
    fun EntriesScreen(
    viewModel: EntriesViewModel = hiltViewModel()
    ) {
    when (val uiState = viewModel.uiState) {
    Loading -> EntriesLoading()
    is Content -> EntryList(entries = uiState.entries)
    }
    }

    View Slide

  60. Kotlin Budapest Meetup – @stewemetal
    Testing in isolation
    • Structure your composables with testing in mind
    @Composable
    fun EntriesScreen(
    viewModel: EntriesViewModel = hiltViewModel()
    ) {
    when (val uiState = viewModel.uiState) {
    Loading -> EntriesLoading()
    is Content -> EntryList(entries = uiState.entries)
    }
    }

    View Slide

  61. Kotlin Budapest Meetup – @stewemetal
    Testing in isolation
    • Structure your composables with testing in mind
    @Composable
    fun EntriesScreen(
    viewModel: EntriesViewModel = hiltViewModel()
    ) {
    when (val uiState = viewModel.uiState) {
    Loading -> EntriesLoading()
    is Content -> EntryList(entries = uiState.entries)
    }
    }

    View Slide

  62. Kotlin Budapest Meetup – @stewemetal
    Testing in isolation
    composeTestRule.apply {
    setContent {
    HydrationTrackerTheme {
    EntryList(
    entries = entries
    )
    }
    }
    onNodeWithTag("entries_list")
    .onChildren()
    .assertCountEquals(entries.size)
    }

    View Slide

  63. Kotlin Budapest Meetup – @stewemetal
    Testing behavior
    • Test behavior with fake callbacks
    @Composable
    fun PredefinedValuesInput(
    onAddDrink: (Int) -> Unit,
    ) {
    Row {
    Button(onClick = { onAddDrink(100) }) { Text("100 ml") }
    Button(onClick = { onAddDrink(250) }) { Text("250 ml") }
    Button(onClick = { onAddDrink(500) }) { Text("500 ml") }
    }
    }

    View Slide

  64. Kotlin Budapest Meetup – @stewemetal
    Testing behavior
    • Test behavior with fake callbacks
    @Composable
    fun PredefinedValuesInput(
    onAddDrink: (Int) -> Unit,
    ) {
    Row {
    Button(onClick = { onAddDrink(100) }) { Text("100 ml") }
    Button(onClick = { onAddDrink(250) }) { Text("250 ml") }
    Button(onClick = { onAddDrink(500) }) { Text("500 ml") }
    }
    }

    View Slide

  65. Kotlin Budapest Meetup – @stewemetal
    Testing behavior
    composeTestRule.apply {
    var addedDrinkValue = 0
    setContent {
    HydrationTrackerTheme {
    PredefinedValuesInput(
    onAddDrink = { value ->
    addedDrinkValue = value
    },
    )
    }
    }
    onNodeWithText("500 ml").performClick()
    assertEquals(500, addedDrinkValue)
    }

    View Slide

  66. Kotlin Budapest Meetup – @stewemetal
    Testing behavior
    composeTestRule.apply {
    var addedDrinkValue = 0
    setContent {
    HydrationTrackerTheme {
    PredefinedValuesInput(
    onAddDrink = { value ->
    addedDrinkValue = value
    },
    )
    }
    }
    onNodeWithText("500 ml").performClick()
    assertEquals(500, addedDrinkValue)
    }

    View Slide

  67. Kotlin Budapest Meetup – @stewemetal
    Testing behavior
    composeTestRule.apply {
    var addedDrinkValue = 0
    setContent {
    HydrationTrackerTheme {
    PredefinedValuesInput(
    onAddDrink = { value ->
    addedDrinkValue = value
    },
    )
    }
    }
    onNodeWithText("500 ml").performClick()
    assertEquals(500, addedDrinkValue)
    }

    View Slide

  68. Kotlin Budapest Meetup – @stewemetal
    Topics to check out
    • Testing Compose animations
    • Testing Views in Composables
    • Synchronization and IdlingResources

    View Slide

  69. Kotlin Budapest Meetup – @stewemetal
    Resources
    • https:!//developer.android.com/jetpack/compose/testing
    • https:!//developer.android.com/jetpack/compose/testing-
    cheatsheet
    • https:!//developer.android.com/jetpack/compose/accessibi
    lity
    • https:!//developer.android.com/jetpack/compose/semantics
    • https:!//youtu.be/kdwofTaEHrs
    • https:!//adbackstage.libsyn.com/episode-171-compose-
    testing
    • https:!//github.com/android/compose-samples
    • https:!//github.com/stewemetal/composehydrationtracker

    View Slide

  70. stewemetal
    How to Test Your Compose UI
    István Juhos
    • Instrumented tests with Rules
    • Testing based on the semantics tree
    • Planned accessibility = good testability
    • Implement composables with testability in mind
    Cover photo by Rob Mulally on Unsplash

    View Slide