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

How to Test Your Compose UI (FOSDEM'23, Brussels)

How to Test Your Compose UI (FOSDEM'23, Brussels)

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. #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
  2. #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 ❌ ❌ ❌ ❌ 🧪
  3. #fosdem23 – @stewemetal istvanjuhos.dev @Composable fun EntryList( entries: List<HydrationEntry> =

    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
  4. #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
  5. #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")
  6. #fosdem23 – @stewemetal istvanjuhos.dev Setting up Compose UI tests @RunWith(AndroidJUnit4!::class)

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

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

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

    class EntryListTest { !// !!... } createComposeRule() @get:Rule val composeTestRule =
  10. #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 =
  11. #fosdem23 – @stewemetal istvanjuhos.dev A simple UI test @get:Rule val

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

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

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

    composeTestRule = createComposeRule() @Test fun entriesExist_entriesDisplayed() { composeTestRule.apply { setContent { HydrationTrackerTheme { EntryList( entries = entries ) } } "// ""... } }
  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 ) } } "// ""... } } private val entries = listOf( HydrationEntry(…), HydrationEntry(…), HydrationEntry(…), )
  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 ) } } "// ""... } } So, how can we act or assert on EntryList? 🤔
  17. #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
  18. #fosdem23 – @stewemetal istvanjuhos.dev The semantics tree 🌳 composeTestRule.apply {

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

    setContent { Row { Text("100 ml") } } } Root Text = '[100 ml]'
  20. #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
  21. #fosdem23 – @stewemetal istvanjuhos.dev Visualizing the semantics tree 🌳👀 Text

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

    In Compose tests onRoot() .printToLog("RowAndTexts") Text = '[100 ml]' Text = '[2023-02-04]' Root
  23. #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
  24. #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
  25. #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
  26. #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
  27. #fosdem23 – @stewemetal istvanjuhos.dev Modifying the semantics tree 🌳 Row(

    modifier = Modifier .semantics { !// !!... } ) { Text("100 ml") Text("2023-02-04") }
  28. #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") }
  29. #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]'
  30. #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]'
  31. #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 🤩
  32. #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]'
  33. #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]'
  34. #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]'
  35. #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]'
  36. #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]' !!...
  37. #fosdem23 – @stewemetal istvanjuhos.dev Actions • performClick • performScrollTo... •

    performTextInput • performGesture • performKeyPress • ... onNodeWithText("100 ml") .performClick()
  38. #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()
  39. #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 🪟 🪟 ☕ &
  40. #fosdem23 – @stewemetal istvanjuhos.dev Hybrid UI testing – ComposeView <androidx.constraintlayout.widget.ConstraintLayout

    > <androidx.appcompat.widget.Toolbar 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” "/> <androidx.compose.ui.platform.ComposeView android:id="@+id/compose_view" android:layout_width="match_parent" android:layout_height= "wrap_content" app:paddingTop=”8dp" app:layout_constraintTop_toBottomOf="@id/toolbar” "/> "</androidx.constraintlayout.widget.ConstraintLayout>
  41. #fosdem23 – @stewemetal istvanjuhos.dev Hybrid UI testing – ComposeView <androidx.constraintlayout.widget.ConstraintLayout

    > <androidx.appcompat.widget.Toolbar 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” "/> <androidx.compose.ui.platform.ComposeView android:id="@+id/compose_view" android:layout_width="match_parent" android:layout_height= "wrap_content" app:paddingTop=”8dp" app:layout_constraintTop_toBottomOf="@id/toolbar” "/> "</androidx.constraintlayout.widget.ConstraintLayout>
  42. #fosdem23 – @stewemetal istvanjuhos.dev Hybrid UI testing – ComposeView <androidx.constraintlayout.widget.ConstraintLayout

    > <androidx.appcompat.widget.Toolbar 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” "/> <androidx.compose.ui.platform.ComposeView android:id="@+id/compose_view" android:layout_width="match_parent" android:layout_height= "wrap_content" app:paddingTop=”8dp" app:layout_constraintTop_toBottomOf="@id/toolbar” "/> "</androidx.constraintlayout.widget.ConstraintLayout>
  43. #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) }
  44. #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) }
  45. #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) }
  46. #fosdem23 – @stewemetal istvanjuhos.dev Hybrid UI testing – ComposeView @RunWith(AndroidJUnit4"::class)

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

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

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

    class DemoComposeViewTest { @get:Rule val androidComposeTestRule = createAndroidComposeRule<ComposeViewDemoActivity>() @Test fun hybrid_ui_customComposeButton_isVisible() { androidComposeTestRule.apply { Espresso.onView(withText("ComposeView Demo Title")) .check(matches(isDisplayed())) onNodeWithText("Demo Button").assertIsDisplayed() } } } ✅ ✅
  50. #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) } } }
  51. #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() } } }, ) } ) }
  52. #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() } } }, ) } ) }
  53. #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 } } }
  54. #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 } } }
  55. #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) ✅ ✅
  56. #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())
  57. #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())
  58. #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())
  59. #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
  60. #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() } ✅ ✅ ✅ ✅❓
  61. #fosdem23 – @stewemetal istvanjuhos.dev Topics to check out • Testing

    animations in Compose 👀 • Synchronization and IdlingResources 🕞 • Screenshot testing with Paparazzi, Showkase, etc. 📸
  62. #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
  63. #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
  64. 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