Upgrade to Pro — share decks privately, control downloads, hide ads and more …

How to Test Your Compose UI

How to Test Your Compose UI

While Compose is easy to adopt, not creating legacy code right at the start of such a journey requires extra planning, awareness, and, most importantly, testing. We'll have a look at how we can test pure Compose UIs as well as hybrid ones that have Views and composables too.

In this talk, we'll learn what the semantics tree is, its relation to the composition, how we can manipulate it in composables using the Semantics modifier, how we can implement composables with testability in mind, and how we can test pure Compose, and hybrid UIs.

I gave this talk in the Kotlin devroom of FOSDEM 2023 in Brussels on 2023.02.04.

István Juhos

February 04, 2023
Tweet

More Decks by István Juhos

Other Decks in Programming

Transcript

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

    View Slide

  2. #fosdem23 – @stewemetal istvanjuhos.dev
    Source of examples
    github.com/stewemetal/composehydrationtracker

    View Slide

  3. #fosdem23 – @stewemetal istvanjuhos.dev
    Testing Views - Recap ☕
    • View(Group) 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. #fosdem23 – @stewemetal istvanjuhos.dev
    Testing Compose UI
    🧪

    View Slide

  5. #fosdem23 – @stewemetal istvanjuhos.dev
    Testing Compose UI
    • No direct access to the Composition
    • No UI objects to look up directly
    • No UI element IDs
    • Not every Composable emits UI




    🧪

    View Slide

  6. #fosdem23 – @stewemetal istvanjuhos.dev
    @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
    #fosdem23 – @stewemetal
    EntryList
    HydrationItem

    View Slide

  7. #fosdem23 – @stewemetal istvanjuhos.dev
    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
    HydrationItem
    EntryList
    #fosdem23 – @stewemetal

    View Slide

  8. #fosdem23 – @stewemetal istvanjuhos.dev
    Setting up Compose UI tests

    View Slide

  9. #fosdem23 – @stewemetal istvanjuhos.dev
    "// Test rules and transitive dependencies:
    androidTestImplementation "androidx.compose.ui:ui-test-junit4"
    "// Needed for createComposeRule, but not createAndroidComposeRule:
    debugImplementation "androidx.compose.ui:ui-test-manifest"
    Setting up Compose UI tests
    implementation platform("androidx.compose:compose-bom:$compose_bom_version")

    View Slide

  10. #fosdem23 – @stewemetal istvanjuhos.dev
    Setting up Compose UI tests
    @RunWith(AndroidJUnit4!::class)
    class EntryListTest {
    !// !!...
    }
    create ComposeRule ()
    Android
    @get:Rule
    val composeTestRule =

    View Slide

  11. #fosdem23 – @stewemetal istvanjuhos.dev
    Setting up Compose UI tests
    @RunWith(AndroidJUnit4!::class)
    class EntryListTest {
    !// !!...
    }
    create ComposeRule ()
    Android
    @get:Rule
    val composeTestRule =

    View Slide

  12. #fosdem23 – @stewemetal istvanjuhos.dev
    Setting up Compose UI tests
    @RunWith(AndroidJUnit4!::class)
    class EntryListTest {
    !// !!...
    }
    create ComposeRule ()
    Android
    @get:Rule
    val composeTestRule =

    View Slide

  13. #fosdem23 – @stewemetal istvanjuhos.dev
    Setting up Compose UI tests
    @RunWith(AndroidJUnit4!::class)
    class EntryListTest {
    !// !!...
    }
    createComposeRule()
    @get:Rule
    val composeTestRule =

    View Slide

  14. #fosdem23 – @stewemetal istvanjuhos.dev
    @get:Rule val composeTestRule = createComposeRule()
    @Test
    fun entriesExist_entriesDisplayed() {
    composeTestRule.apply {
    setContent {
    HydrationTrackerTheme {
    EntryList(
    entries = entries
    )
    }
    }
    "// ""...
    }
    }
    A simple UI test
    createComposeRule()
    @get:Rule val composeTestRule =

    View Slide

  15. #fosdem23 – @stewemetal istvanjuhos.dev
    A simple UI test
    @get:Rule val composeTestRule = createComposeRule()
    @Test
    fun entriesExist_entriesDisplayed() {
    composeTestRule.apply {
    setContent {
    HydrationTrackerTheme {
    EntryList(
    entries = entries
    )
    }
    }
    "// ""...
    }
    }

    View Slide

  16. #fosdem23 – @stewemetal istvanjuhos.dev
    A simple UI test
    @get:Rule val composeTestRule = createComposeRule()
    @Test
    fun entriesExist_entriesDisplayed() {
    composeTestRule.apply {
    setContent {
    HydrationTrackerTheme {
    EntryList(
    entries = entries
    )
    }
    }
    "// ""...
    }
    }

    View Slide

  17. #fosdem23 – @stewemetal istvanjuhos.dev
    A simple UI test
    @get:Rule val composeTestRule = createComposeRule()
    @Test
    fun entriesExist_entriesDisplayed() {
    composeTestRule.apply {
    setContent {
    HydrationTrackerTheme {
    EntryList(
    entries = entries
    )
    }
    }
    "// ""...
    }
    }

    View Slide

  18. #fosdem23 – @stewemetal istvanjuhos.dev
    A simple UI test
    @get:Rule val composeTestRule = createComposeRule()
    @Test
    fun entriesExist_entriesDisplayed() {
    composeTestRule.apply {
    setContent {
    HydrationTrackerTheme {
    EntryList(
    entries = entries
    )
    }
    }
    "// ""...
    }
    }

    View Slide

  19. #fosdem23 – @stewemetal istvanjuhos.dev
    A simple UI test
    @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

  20. #fosdem23 – @stewemetal istvanjuhos.dev
    A simple UI test
    @get:Rule val composeTestRule = createComposeRule()
    @Test
    fun entriesExist_entriesDisplayed() {
    composeTestRule.apply {
    setContent {
    HydrationTrackerTheme {
    EntryList(
    entries = entries
    )
    }
    }
    "// ""...
    }
    }
    So, how can we act or assert on EntryList? 🤔

    View Slide

  21. #fosdem23 – @stewemetal istvanjuhos.dev
    The semantics tree 🌳

    View Slide

  22. #fosdem23 – @stewemetal istvanjuhos.dev
    The semantics tree 🌳
    • A parallel tree next to the Composition 🌲🌳
    • Used for accessibility and testing
    • Composables (can) contribute to the semantics tree

    View Slide

  23. #fosdem23 – @stewemetal istvanjuhos.dev
    The semantics tree 🌳
    composeTestRule.apply {
    setContent {
    Text("100 ml")
    }
    }
    Semantics tree
    root node
    Semantic node with text
    "100 ml"

    View Slide

  24. #fosdem23 – @stewemetal istvanjuhos.dev
    The semantics tree 🌳
    composeTestRule.apply {
    setContent {
    Text("100 ml")
    }
    }
    Root
    Text = '[100 ml]'

    View Slide

  25. #fosdem23 – @stewemetal istvanjuhos.dev
    The semantics tree 🌳
    composeTestRule.apply {
    setContent {
    Row {
    Text("100 ml")
    }
    }
    }
    Root
    Text = '[100 ml]'

    View Slide

  26. #fosdem23 – @stewemetal istvanjuhos.dev
    The semantics tree 🌳
    composeTestRule.apply {
    setContent {
    Row {
    Text("100 ml")
    Text("2023-02-04")
    }
    }
    }
    Text = '[100 ml]' Text = '[2023-02-04]'
    Root

    View Slide

  27. #fosdem23 – @stewemetal istvanjuhos.dev
    Visualizing the semantics tree 🌳👀
    Text = '[100 ml]' Text = '[2023-02-04]'
    Root
    Row {
    Text("100 ml")
    Text("2023-02-04")
    }

    View Slide

  28. #fosdem23 – @stewemetal istvanjuhos.dev
    Visualizing the semantics tree 🌳👀
    • In Compose tests
    onRoot()
    .printToLog("RowAndTexts") Text = '[100 ml]' Text = '[2023-02-04]'
    Root

    View Slide

  29. #fosdem23 – @stewemetal istvanjuhos.dev
    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 = '[2023-02-04]'
    Actions = [GetTextLayoutResult]
    Text = '[100 ml]' Text = '[2023-02-04]'
    Root

    View Slide

  30. #fosdem23 – @stewemetal istvanjuhos.dev
    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 = '[2023-02-04]'
    Actions = [GetTextLayoutResult]
    Text = '[100 ml]' Text = '[2023-02-04]'
    Root

    View Slide

  31. #fosdem23 – @stewemetal istvanjuhos.dev
    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 = '[2023-02-04]'
    Actions = [GetTextLayoutResult]
    Text = '[100 ml]' Text = '[2023-02-04]'
    Root

    View Slide

  32. #fosdem23 – @stewemetal istvanjuhos.dev
    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 = '[2023-02-04]'
    Actions = [GetTextLayoutResult]
    Text = '[100 ml]' Text = '[2023-02-04]'
    Root

    View Slide

  33. #fosdem23 – @stewemetal istvanjuhos.dev
    Visualizing the semantics tree 🌳👀
    • In Android Studio
    • Layout Inspector

    View Slide

  34. #fosdem23 – @stewemetal istvanjuhos.dev
    Modifying the semantics tree 🌳
    Row(
    modifier = Modifier
    .semantics {
    !// !!...
    }
    ) {
    Text("100 ml")
    Text("2023-02-04")
    }

    View Slide

  35. #fosdem23 – @stewemetal istvanjuhos.dev
    Root
    Text = '[100 ml]' Text = '[2023-02-04]'
    Modifying the semantics tree 🌳
    Row(
    modifier = Modifier
    .semantics {
    !// !!...
    }
    ) {
    Text("100 ml")
    Text("2023-02-04")
    }

    View Slide

  36. #fosdem23 – @stewemetal istvanjuhos.dev
    Modifying the semantics tree 🌳
    Row(
    modifier = Modifier
    .semantics {
    contentDescription = "List item"
    }
    ) {
    Text("100 ml")
    Text("2023-02-04")
    }
    Root
    Text = '[100 ml]' Text = '[2023-02-04]'
    ContentDescription =
    '[List item]'

    View Slide

  37. #fosdem23 – @stewemetal istvanjuhos.dev
    Modifying the semantics tree 🌳
    Row(
    modifier = Modifier
    .semantics {
    testTag = "List item"
    }
    ) {
    Text("100 ml")
    Text("2023-02-04")
    }
    Root
    Text = '[100 ml]' Text = '[2023-02-04]'
    Tag =
    '[List item]'

    View Slide

  38. #fosdem23 – @stewemetal istvanjuhos.dev
    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

  39. #fosdem23 – @stewemetal istvanjuhos.dev
    Compose testing API

    View Slide

  40. #fosdem23 – @stewemetal istvanjuhos.dev
    Select nodes - Finders
    • onRoot
    onRoot()
    .printToLog(”TAG") Root
    Text = '[100 ml, 2023-02-04]'
    Tag = '[List item]'
    ContentDescription =
    '[100 ml on 2023-02-04]'

    View Slide

  41. #fosdem23 – @stewemetal istvanjuhos.dev
    Select nodes - Finders
    • onNodeWithTag
    onNodeWithTag("List item")
    Root
    Text = '[100 ml, 2023-02-04]'
    Tag = '[List item]'
    ContentDescription =
    '[100 ml on 2023-02-04]'

    View Slide

  42. #fosdem23 – @stewemetal istvanjuhos.dev
    Select nodes - Finders
    • onNodeWithContentDescription
    onNodeWithContentDescription(
    "100 ml on 2023-02-04",
    )
    Root
    Text = '[100 ml, 2023-02-04]'
    Tag = '[List item]'
    ContentDescription =
    '[100 ml on 2023-02-04]'

    View Slide

  43. #fosdem23 – @stewemetal istvanjuhos.dev
    Select nodes - Finders
    • onNodeWithText
    onNodeWithText(
    "100 ml",
    )
    Root
    Text = '[100 ml, 2023-02-04]'
    Tag = '[List item]'
    ContentDescription =
    '[100 ml on 2023-02-04]'

    View Slide

  44. #fosdem23 – @stewemetal istvanjuhos.dev
    Select nodes - Finders
    • onAllNodesWith...
    Tag = '[entries_list]'
    onAllNodesNodeWithTag("List item")
    Tag = '[List item]'
    Text = '[100 ml, 2023-02-04]'
    Tag = '[List item]'
    Text = '[500 ml, 2022-07-04]'
    !!...

    View Slide

  45. #fosdem23 – @stewemetal istvanjuhos.dev
    Actions
    • performClick
    • performScrollTo...
    • performTextInput
    • performGesture
    • performKeyPress
    • ...
    onNodeWithText("100 ml")
    .performClick()

    View Slide

  46. #fosdem23 – @stewemetal istvanjuhos.dev
    Actions
    • performClick
    • performScrollTo...
    • performTextInput
    • performGesture
    • performKeyPress
    • ...
    Role = 'Button'
    Focused = 'false'
    Text = '[100 ml]'
    Actions = [OnClick, GetTextLayoutResult]
    MergeDescendants = 'true'
    onNodeWithText("100 ml")
    .performClick()

    View Slide

  47. #fosdem23 – @stewemetal istvanjuhos.dev
    Compose testing cheatseet
    developer.android.com/jetpack/compose/testing-cheatsheet

    View Slide

  48. #fosdem23 – @stewemetal istvanjuhos.dev
    Hybrid UI testing

    View Slide

  49. #fosdem23 – @stewemetal istvanjuhos.dev
    Hybrid UI testing
    • Adding Compose content to existing View-based layouts
    • Adding existing Views to the composition
    • Compose & Espresso in the same tests
    🪟
    🪟

    &

    View Slide

  50. #fosdem23 – @stewemetal istvanjuhos.dev
    Hybrid UI testing – ComposeView

    View Slide

  51. #fosdem23 – @stewemetal istvanjuhos.dev
    Hybrid UI testing – ComposeView

    android:id="@+id/toolbar”
    style="@style/Widget.MaterialComponents.Toolbar.Primary"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:layout_constraintTop_toTopOf="parent” "/>
    android:id="@+id/compose_view"
    android:layout_width="match_parent"
    android:layout_height= "wrap_content"
    app:paddingTop=”8dp"
    app:layout_constraintTop_toBottomOf="@id/toolbar” "/>
    "

    View Slide

  52. #fosdem23 – @stewemetal istvanjuhos.dev
    Hybrid UI testing – ComposeView

    android:id="@+id/toolbar”
    style="@style/Widget.MaterialComponents.Toolbar.Primary"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:layout_constraintTop_toTopOf="parent” "/>
    android:id="@+id/compose_view"
    android:layout_width="match_parent"
    android:layout_height= "wrap_content"
    app:paddingTop=”8dp"
    app:layout_constraintTop_toBottomOf="@id/toolbar” "/>
    "

    View Slide

  53. #fosdem23 – @stewemetal istvanjuhos.dev
    Hybrid UI testing – ComposeView

    android:id="@+id/toolbar”
    style="@style/Widget.MaterialComponents.Toolbar.Primary"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:layout_constraintTop_toTopOf="parent” "/>
    android:id="@+id/compose_view"
    android:layout_width="match_parent"
    android:layout_height= "wrap_content"
    app:paddingTop=”8dp"
    app:layout_constraintTop_toBottomOf="@id/toolbar” "/>
    "

    View Slide

  54. #fosdem23 – @stewemetal istvanjuhos.dev
    Hybrid UI testing – ComposeView
    class ComposeViewDemoActivity : AppCompatActivity() {
    private lateinit var binding: ActivityComposeViewDemoBinding
    "// ""...
    }
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityComposeViewDemoBinding.inflate(layoutInflater)
    setContentView(binding.root)
    }

    View Slide

  55. #fosdem23 – @stewemetal istvanjuhos.dev
    Hybrid UI testing – ComposeView
    binding.toolbar.title = "ComposeView Demo Title"
    binding.composeView.apply {
    setViewCompositionStrategy(DisposeOnDetachedFromWindow)
    setContent {
    CustomButton(
    text = "Demo Button",
    modifier = Modifier
    .padding(horizontal = 8.dp)
    .testTag("demo_custom_button"),
    ) {}
    }
    }
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityComposeViewDemoBinding.inflate(layoutInflater)
    setContentView(binding.root)
    }

    View Slide

  56. #fosdem23 – @stewemetal istvanjuhos.dev
    Hybrid UI testing – ComposeView
    binding.toolbar.title = "ComposeView Demo Title"
    binding.composeView.apply {
    setViewCompositionStrategy(DisposeOnDetachedFromWindow)
    setContent {
    CustomButton(
    text = "Demo Button",
    modifier = Modifier
    .padding(horizontal = 8.dp)
    .testTag("demo_custom_button"),
    ) {}
    }
    }
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityComposeViewDemoBinding.inflate(layoutInflater)
    setContentView(binding.root)
    }

    View Slide

  57. #fosdem23 – @stewemetal istvanjuhos.dev
    Hybrid UI testing – ComposeView
    @RunWith(AndroidJUnit4"::class)
    class DemoComposeViewTest {
    @get:Rule
    val androidComposeTestRule =
    createAndroidComposeRule()
    @Test
    fun hybrid_ui_customComposeButton_isVisible() {
    androidComposeTestRule.apply {
    Espresso.onView(withText("ComposeView Demo Title"))
    .check(matches(isDisplayed()))
    onNodeWithText("Demo Button").assertIsDisplayed()
    }
    }
    }

    View Slide

  58. #fosdem23 – @stewemetal istvanjuhos.dev
    Hybrid UI testing – ComposeView
    @RunWith(AndroidJUnit4"::class)
    class DemoComposeViewTest {
    @get:Rule
    val androidComposeTestRule =
    createAndroidComposeRule()
    @Test
    fun hybrid_ui_customComposeButton_isVisible() {
    androidComposeTestRule.apply {
    Espresso.onView(withText("ComposeView Demo Title"))
    .check(matches(isDisplayed()))
    onNodeWithText("Demo Button").assertIsDisplayed()
    }
    }
    }

    View Slide

  59. #fosdem23 – @stewemetal istvanjuhos.dev
    Hybrid UI testing – ComposeView
    @RunWith(AndroidJUnit4"::class)
    class DemoComposeViewTest {
    @get:Rule
    val androidComposeTestRule =
    createAndroidComposeRule()
    @Test
    fun hybrid_ui_customComposeButton_isVisible() {
    androidComposeTestRule.apply {
    Espresso.onView(withText("ComposeView Demo Title"))
    .check(matches(isDisplayed()))
    onNodeWithText("Demo Button").assertIsDisplayed()
    }
    }
    }

    View Slide

  60. #fosdem23 – @stewemetal istvanjuhos.dev
    Hybrid UI testing – ComposeView
    @RunWith(AndroidJUnit4"::class)
    class DemoComposeViewTest {
    @get:Rule
    val androidComposeTestRule =
    createAndroidComposeRule()
    @Test
    fun hybrid_ui_customComposeButton_isVisible() {
    androidComposeTestRule.apply {
    Espresso.onView(withText("ComposeView Demo Title"))
    .check(matches(isDisplayed()))
    onNodeWithText("Demo Button").assertIsDisplayed()
    }
    }
    }


    View Slide

  61. #fosdem23 – @stewemetal istvanjuhos.dev
    Hybrid UI testing - AndroidView

    View Slide

  62. #fosdem23 – @stewemetal istvanjuhos.dev
    Hybrid UI testing - AndroidView
    class CustomButtonView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    ) : ConstraintLayout(context, attrs, defStyleAttr) {
    "// ""...
    LayoutInflater.from(context).inflate(R.layout.view_custom_button, this, true)
    "// ""...
    fun setText(text: String) { button.text = text }
    override fun setOnClickListener(listener: OnClickListener?) {
    button.setOnClickListener { listener"?.onClick(button) }
    }
    }

    View Slide

  63. #fosdem23 – @stewemetal istvanjuhos.dev
    Hybrid UI testing - AndroidView
    @Composable
    fun AndroidViewDemo(
    onButtonClick: () -> Unit,
    ) {
    Scaffold(
    topBar = { TopAppBar(title = { Text("AndroidView Demo Title") }) },
    content = { padding ->
    AndroidView(
    modifier = Modifier.padding(padding).wrapContentHeight(),
    factory = { context ->
    CustomButtonView(context).apply {
    setText("Demo Button")
    setOnClickListener { onButtonClick() }
    }
    },
    )
    }
    )
    }

    View Slide

  64. #fosdem23 – @stewemetal istvanjuhos.dev
    Hybrid UI testing - AndroidView
    @Composable
    fun AndroidViewDemo(
    onButtonClick: () -> Unit,
    ) {
    Scaffold(
    topBar = { TopAppBar(title = { Text("AndroidView Demo Title") }) },
    content = { padding ->
    AndroidView(
    modifier = Modifier.padding(padding).wrapContentHeight(),
    factory = { context ->
    CustomButtonView(context).apply {
    setText("Demo Button")
    setOnClickListener { onButtonClick() }
    }
    },
    )
    }
    )
    }

    View Slide

  65. #fosdem23 – @stewemetal istvanjuhos.dev
    Hybrid UI testing - AndroidView
    @get:Rule
    val composeTestRule = createComposeRule()
    @Test
    fun composable_withAndroidView() {
    composeTestRule.apply {
    "// ""...
    }
    }
    var buttonClicked = false
    setContent {
    HydrationTrackerTheme {
    AndroidViewDemo { buttonClicked = true }
    }
    }

    View Slide

  66. #fosdem23 – @stewemetal istvanjuhos.dev
    Hybrid UI testing - AndroidView
    onNodeWithText("AndroidView Demo Title").assertIsDisplayed()
    Espresso.onView(withText("Demo Button")).check(matches(isDisplayed()))
    Espresso.onView(withText("Demo Button")).perform(click())
    assertEquals(true, buttonClicked)
    var buttonClicked = false
    setContent {
    HydrationTrackerTheme {
    AndroidViewDemo { buttonClicked = true }
    }
    }

    View Slide

  67. #fosdem23 – @stewemetal istvanjuhos.dev
    Hybrid UI testing - AndroidView
    var buttonClicked = false
    setContent {
    HydrationTrackerTheme {
    AndroidViewDemo { buttonClicked = true }
    }
    }
    onNodeWithText("AndroidView Demo Title").assertIsDisplayed()
    Espresso.onView(withText("Demo Button")).check(matches(isDisplayed()))
    Espresso.onView(withText("Demo Button")).perform(click())
    assertEquals(true, buttonClicked)


    View Slide

  68. #fosdem23 – @stewemetal istvanjuhos.dev
    Hybrid UI testing - AndroidView
    var buttonClicked = false
    setContent {
    HydrationTrackerTheme {
    AndroidViewDemo { buttonClicked = true }
    }
    }
    onNodeWithText("AndroidView Demo Title").assertIsDisplayed()
    Espresso.onView(withText("Demo Button")).check(matches(isDisplayed()))
    assertEquals(true, buttonClicked)


    ✅❓
    Espresso.onView(withText("Demo Button")).perform(click())

    View Slide

  69. #fosdem23 – @stewemetal istvanjuhos.dev
    Hybrid UI testing - AndroidView
    var buttonClicked = false
    setContent {
    HydrationTrackerTheme {
    AndroidViewDemo { buttonClicked = true }
    }
    }
    onNodeWithText("AndroidView Demo Title").assertIsDisplayed()
    Espresso.onView(withText("Demo Button")).check(matches(isDisplayed()))
    assertEquals(true, buttonClicked)


    ✅❓

    Espresso.onView(withText("Demo Button")).perform(click())

    View Slide

  70. #fosdem23 – @stewemetal istvanjuhos.dev
    Hybrid UI testing - AndroidView
    Doesn’t work as of Jetpack Compose BOM 2023.01.00 🙁
    https:!//issuetracker.google.com/issues/215231631
    Espresso.onView(withText("Demo Button")).perform(click())

    View Slide

  71. #fosdem23 – @stewemetal istvanjuhos.dev
    Hybrid UI testing - AndroidView
    Espresso.onView(withText("Demo Button")).check { view, _ ->
    view.performClick()
    }
    Unstable workaround
    https:!//issuetracker.google.com/issues/215231631

    View Slide

  72. #fosdem23 – @stewemetal istvanjuhos.dev
    Hybrid UI testing - AndroidView
    var buttonClicked = false
    setContent {
    HydrationTrackerTheme {
    AndroidViewDemo { buttonClicked = true }
    }
    }
    onNodeWithText("AndroidView Demo Title").assertIsDisplayed()
    Espresso.onView(withText("Demo Button")).check(matches(isDisplayed()))
    assertEquals(true, buttonClicked)
    Espresso.onView(withText("Demo Button")).check { view, _ ->
    view.performClick()
    }



    ✅❓

    View Slide

  73. #fosdem23 – @stewemetal istvanjuhos.dev
    Topics to check out
    • Testing animations in Compose 👀
    • Synchronization and IdlingResources 🕞
    • Screenshot testing with Paparazzi, Showkase, etc. 📸

    View Slide

  74. #fosdem23 – @stewemetal istvanjuhos.dev
    Resources
    • developer.android.com/jetpack/compose/testing
    • developer.android.com/jetpack/compose/testing-cheatsheet
    • developer.android.com/jetpack/compose/accessibility
    • developer.android.com/jetpack/compose/semantics
    • youtu.be/kdwofTaEHrs
    • adbackstage.libsyn.com/episode-171-compose-testing
    • github.com/android/compose-samples
    • adavis.info/2021/09/testing-hybrid-jetpack-compose-apps.html
    • github.com/stewemetal/composehydrationtracker

    View Slide

  75. #fosdem23 – @stewemetal istvanjuhos.dev
    Resources
    • developer.android.com/jetpack/compose/testing
    • developer.android.com/jetpack/compose/testing-cheatsheet
    • developer.android.com/jetpack/compose/accessibility
    • developer.android.com/jetpack/compose/semantics
    • youtu.be/kdwofTaEHrs
    • adbackstage.libsyn.com/episode-171-compose-testing
    • github.com/android/compose-samples
    • adavis.info/2021/09/testing-hybrid-jetpack-compose-apps.html
    • github.com/stewemetal/composehydrationtracker
    istvanjuhos.dev

    View Slide

  76. stewemetal
    istvanjuhos.dev
    How to Test Your Compose UI István Juhos
    • Testing based on the semantics tree
    • Planned accessibility = good testability
    • Hybrid UI testing is supported out of the box
    (although there are issues currently)
    Cover photo by Rob Mulally on Unsplash

    View Slide