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

Workshop: Android UI Testing

Workshop: Android UI Testing

Mobile apps are growing, we have new features after every big release. Testing the app manually is time consuming. It means that it is time to integrate fast and reliable automated tests into your development process.

In this talk, we will discuss the following topics:
– How to create fast and reliable UI tests
– How to avoid flaky tests
– How to cover applications which include traditional and Jetpack Compose views and screens
– How to share UI tests between local and instrumentation tests
– How DSL can speed up adding stable UI tests to the project
– How to combine interaction and pixel-perfectness tests
– What tools to use for effective testing of different application components

Alex Zhukovich

May 10, 2023
Tweet

More Decks by Alex Zhukovich

Other Decks in Programming

Transcript

  1. @alex_zhukovich
    https://alexzh.com/
    Modern Android


    UI Testing


    Workshop

    View Slide

  2. View Slide

  3. Android Testing World

    View Slide

  4. ANDROID TESTS
    LOCAL INSTRUMENTATION

    View Slide

  5. ANDROID TESTS
    LOCAL INSTRUMENTATION
    UI* Non-UI

    View Slide

  6. ANDROID TESTS
    LOCAL INSTRUMENTATION
    UI* Non-UI UI Non-UI

    View Slide

  7. app.apk
    tests.apk
    BUILD APKs

    View Slide

  8. app.apk tests.apk
    >_ adb shell


    am instrument

    View Slide

  9. app.apk tests.apk
    >_ adb shell


    am instrument

    View Slide

  10. ACTIVITY FRAGMENT

    View Slide

  11. ACTIVITY FRAGMENT
    COMPOSABLE
    FUNCTION

    View Slide

  12. View Slide

  13. View Slide

  14. launchActivity
    launchFragmentInContainer
    ComposeContentTestRule
    ActivityScenario.launch(


    MainActivity::class.java


    )


    View Slide

  15. launchActivity
    launchFragmentInContainer
    ComposeContentTestRule
    val activityOptions = bundleOf(


    Pair("param-1", 1),


    Pair("param-2", 2),


    Pair("param-3", 3),


    )


    val scenario = ActivityScenario.launch(


    MainActivity::class.java,


    activityOptions


    )


    scenario.moveToState(Lifecycle.State.STARTED)


    ActivityScenario.launch(


    MainActivity::class.java


    )


    View Slide

  16. launchActivity
    launchFragmentInContainer
    ComposeContentTestRule
    val scenario = launchFragmentInContainer(


    fragmentArgs: Bundle? = null,


    @StyleRes themeResId: Int =

    R.style.FragmentScenarioEmptyFragmentActivityTheme,


    initialState: Lifecycle.State =

    Lifecycle.State.RESUMED,


    factory: FragmentFactory? = null


    )


    scenario.moveToState(Lifecycle.State.STARTED)


    debugImplementation "androidx.fragment:fragment-testing:1.4.0"

    View Slide

  17. launchActivity
    launchFragmentInContainer
    ComposeContentTestRule
    Activity
    Fragment

    View Slide

  18. launchActivity
    launchFragmentInContainer
    ComposeContentTestRule
    androidx.fragment.app.testing.FragmentScenario$EmptyFragmentActivity
    Activity
    Fragment
    Compilation
    Added to AndroidManifest

    View Slide

  19. launchActivity
    launchFragmentInContainer
    ComposeContentTestRule
    @get:Rule


    val composeTestRule = createComposeRule()


    composeTestRule.apply {


    setContent { LoginScreen() }


    onNodeWithTag("email")


    .performTextInput("[email protected]")


    onNodeWithTag("password")


    .performTextInput("password")


    onNodeWithTag("login")


    .performClick()


    ...


    }
    debugImplementation "androidx.compose.ui:ui-test-manifest:1:0:5"

    View Slide

  20. launchActivity
    launchFragmentInContainer
    ComposeContentTestRule
    Component
    Activity
    @Composable

    View Slide

  21. launchActivity
    launchFragmentInContainer
    ComposeContentTestRule
    androidx.activity.ComponentActivity
    Component
    Activity
    @Composable
    Compilation
    Added to AndroidManifest

    View Slide

  22. Testing Android Views

    View Slide

  23. COMPONENT HAS ID PROPERTIES REAL CLOCK

    View Slide

  24. onView(withId(R.id.email))


    .perform(replaceText(EMAIL)


    onView(withId(R.id.password))


    .perform(replaceText(PASSWORD)


    onView(withId(R.id.login))


    .check(matchers(isDisplayed()))

    View Slide

  25. Testing Composable code

    View Slide

  26. NO ID

    OF COMPONENT
    SEMANTIC
    PROPERTIES
    VIRTUAL CLOCK

    View Slide

  27. class SubscribeBoxTest {


    @get:Rule


    val composeTestRule = createComposeRule()


    @Test


    fun shouldDisplaySnackbar_whenEmailIsEnteredAndSubscribeClicked() {


    composeTestRule.apply {


    setContent { SubscribeBox() }


    onNode(hasTestTag("subscribe_email"))


    .performTextInput("[email protected]")


    onNode(hasText("SUBSCRIBE"))


    .performClick()


    onNode(hasText("You successfully subscribed"))


    .assertIsDisplayed()


    }


    }


    }


    View Slide

  28. class SubscribeBoxTest {


    @get:Rule


    val composeTestRule = createComposeRule()


    @Test


    fun shouldDisplaySnackbar_whenEmailIsEnteredAndSubscribeClicked() {


    composeTestRule.apply {


    setContent { SubscribeBox() }


    onNode(hasTestTag("subscribe_email"))


    .performTextInput("[email protected]")


    onNode(hasText("SUBSCRIBE"))


    .performClick()


    onNode(hasText("You successfully subscribed"))


    .assertIsDisplayed()


    }


    }


    }


    Finder Matcher

    View Slide

  29. class SubscribeBoxTest {


    @get:Rule


    val composeTestRule = createComposeRule()


    @Test


    fun shouldDisplaySnackbar_whenEmailIsEnteredAndSubscribeClicked() {


    composeTestRule.apply {


    setContent { SubscribeBox() }


    onNode(hasTestTag("subscribe_email"))


    .performTextInput("[email protected]")


    onNode(hasText("SUBSCRIBE"))


    .performClick()


    onNode(hasText("You successfully subscribed"))


    .assertIsDisplayed()


    }


    }


    }


    Finder Matcher
    Action

    View Slide

  30. class SubscribeBoxTest {


    @get:Rule


    val composeTestRule = createComposeRule()


    @Test


    fun shouldDisplaySnackbar_whenEmailIsEnteredAndSubscribeClicked() {


    composeTestRule.apply {


    setContent { SubscribeBox() }


    onNode(hasTestTag("subscribe_email"))


    .performTextInput("[email protected]")


    onNode(hasText("SUBSCRIBE"))


    .performClick()


    onNode(hasText("You successfully subscribed"))


    .assertIsDisplayed()


    }


    }


    }


    Finder Matcher
    Action
    Assertion

    View Slide

  31. MERGED NODE TREE
    UNMERGED NODE TREE
    Printing with useUnmergedTree = ‘false’


    Node #1 at (l=0.0, t=74.0, r=1080.0, b=2012.0)px


    |-Node #2 at (l=247.0, t=956.0, r=834.0, b=1130.0)px


    Role = 'Button'


    Focused = 'false'


    ContentDescription = '[Schedule a task]'


    Text = '[Schedule a task]'


    Actions = [OnClick, GetTextLayoutResult]


    MergeDescendants = 'true'
    Printing with useUnmergedTree = ‘true’


    Node #1 at (l=0.0, t=74.0, r=1080.0, b=2012.0)px


    |-Node #2 at (l=247.0, t=956.0, r=834.0, b=1130.0)px


    Role = 'Button'


    Focused = 'false'


    Actions = [OnClick]


    MergeDescendants = 'true'


    |-Node #4 at (l=321.0, t=1006.0, r=395.0, b=1080.0)px


    | ContentDescription = '[Schedule a task]'


    | Role = 'Image'


    |-Node #5 at (l=420.0, t=1014.0, r=785.0, b=1072.0)px


    Text = '[Schedule a task]'


    Actions = [GetTextLayoutResult]

    View Slide

  32. MERGED NODE TREE
    UNMERGED NODE TREE
    Printing with useUnmergedTree = ‘true’


    Node #1 at (l=0.0, t=74.0, r=1080.0, b=2012.0)px


    |-Node #2 at (l=247.0, t=956.0, r=834.0, b=1130.0)px


    Role = 'Button'


    Focused = 'false'


    Actions = [OnClick]


    MergeDescendants = 'true'


    |-Node #4 at (l=321.0, t=1006.0, r=395.0, b=1080.0)px


    | ContentDescription = '[Schedule a task]'


    | Role = 'Image'


    |-Node #5 at (l=420.0, t=1014.0, r=785.0, b=1072.0)px


    Text = '[Schedule a task]'


    Actions = [GetTextLayoutResult]
    Printing with useUnmergedTree = ‘false’


    Node #1 at (l=0.0, t=74.0, r=1080.0, b=2012.0)px


    |-Node #2 at (l=247.0, t=956.0, r=834.0, b=1130.0)px


    Role = 'Button'


    Focused = 'false'


    ContentDescription = '[Schedule a task]'


    Text = '[Schedule a task]'


    Actions = [OnClick, GetTextLayoutResult]


    MergeDescendants = 'true'
    composeTestRule


    .onRoot(useUnmergedTree = false)

    .printToLog("MERGED")


    composeTestRule


    .onRoot(useUnmergedTree = true)


    .printToLog("UNMERGED")


    View Slide

  33. MERGED NODE TREE
    UNMERGED NODE TREE
    onNode(hasText("Schedule a task"), useUnmergedTree = true)


    .assertHasClickAction()


    Error
    onNode(hasText("Schedule a task"))


    .assertHasClickAction()


    useUnmergedTree = false
    Printing with useUnmergedTree = ‘true’


    Node #1 at (l=0.0, t=74.0, r=1080.0, b=2012.0)px


    |-Node #2 at (l=247.0, t=956.0, r=834.0, b=1130.0)px


    Role = 'Button'


    Focused = 'false'


    Actions = [OnClick]


    MergeDescendants = 'true'


    |-Node #4 at (l=321.0, t=1006.0, r=395.0, b=1080.0)px


    | ContentDescription = '[Schedule a task]'


    | Role = 'Image'


    |-Node #5 at (l=420.0, t=1014.0, r=785.0, b=1072.0)px


    Text = '[Schedule a task]'


    Actions = [GetTextLayoutResult]
    Printing with useUnmergedTree = ‘false’


    Node #1 at (l=0.0, t=74.0, r=1080.0, b=2012.0)px


    |-Node #2 at (l=247.0, t=956.0, r=834.0, b=1130.0)px


    Role = 'Button'


    Focused = 'false'


    ContentDescription = '[Schedule a task]'


    Text = '[Schedule a task]'


    Actions = [OnClick, GetTextLayoutResult]


    MergeDescendants = 'true'
    composeTestRule


    .onRoot(useUnmergedTree = false)

    .printToLog("MERGED")


    composeTestRule


    .onRoot(useUnmergedTree = true)


    .printToLog("UNMERGED")


    View Slide

  34. onAllNodesWithContentDescription("Happy")[0]


    .assert(hasText(“08:30"))


    onAllNodes(hasContentDescription("Happy"))[1]


    .assert(hasText("19:45"))


    onNodeWithText("Add")


    .performClick()

    View Slide

  35. View Slide

  36. @Preview


    @Composable


    fun Preview_MultipleRadioButtons() {


    val selectedValue = remember { mutableStateOf("") }


    val isSelectedItem: (String) -> Boolean = { selectedValue.value == it }


    val onChangeState: (String) -> Unit = { selectedValue.value = it }


    val items = listOf("Item 1", "Item 2", "Item 3", "Item 4", "Item 5")


    Column(Modifier.padding(8.dp)) {


    Text(text = "Selected value: ${selectedValue.value.ifEmpty { "NONE" }}")


    items.forEach { item ->


    Row(


    verticalAlignment = Alignment.CenterVertically,


    modifier = Modifier.selectable(


    selected = isSelectedItem(item),


    onClick = { onChangeState(item) },


    role = Role.RadioButton


    ).padding(8.dp)


    ) {


    RadioButton(


    selected = isSelectedItem(item),


    onClick = null


    )


    Text(


    text = item,


    modifier = Modifier.fillMaxWidth()


    )


    }


    }


    }


    }


    View Slide

  37. @Preview


    @Composable


    fun Preview_MultipleRadioButtons() {


    val selectedValue = remember { mutableStateOf("") }


    val isSelectedItem: (String) -> Boolean = { selectedValue.value == it }


    val onChangeState: (String) -> Unit = { selectedValue.value = it }


    val items = listOf("Item 1", "Item 2", "Item 3", "Item 4", "Item 5")


    Column(Modifier.padding(8.dp)) {


    Text(text = "Selected value: ${selectedValue.value.ifEmpty { "NONE" }}")


    items.forEach { item ->


    Row(


    verticalAlignment = Alignment.CenterVertically,


    modifier = Modifier.selectable(


    selected = isSelectedItem(item),


    onClick = { onChangeState(item) },


    role = Role.RadioButton


    ).padding(8.dp)


    ) {


    RadioButton(


    selected = isSelectedItem(item),


    onClick = null


    )


    Text(


    text = item,


    modifier = Modifier.fillMaxWidth()


    )


    }


    }


    }


    }


    View Slide

  38. @ExperimentalFoundationApi


    class RadioButtonTest {


    @get:Rule


    val composeTestRule = createComposeRule()


    @Test


    fun firstItemSelectedByDefault_whenThirdItemIsSelected_thenSelectedValueDisplayed() {


    composeTestRule.apply {


    setContent { Demo_RadioButton () }


    onNodeWithRoleAndText(Role.RadioButton, "Item 1")


    .assertIsSelected()


    onNodeWithText("Item 3")


    .performClick()


    onNodeWithRoleAndText(Role.RadioButton, "Item 3")


    .assertIsSelected()


    onNodeWithText("Selected value: Item 3")


    .assertIsDisplayed()


    }


    }


    private fun withRole(role: Role): SemanticsMatcher {


    return SemanticsMatcher("${SemanticsProperties.Role.name} contains '$role'") {


    it.config.getOrNull(SemanticsProperties.Role) == role


    }


    }


    private fun SemanticsNodeInteractionsProvider.onNodeWithRoleAndText(


    role: Role,


    text: String


    ) = onNode(


    withRole(role)


    .and(isSelectable())


    .and(isEnabled())


    .and(hasText(text))


    )


    }


    View Slide

  39. @ExperimentalFoundationApi


    class RadioButtonTest {


    @get:Rule


    val composeTestRule = createComposeRule()


    @Test


    fun firstItemSelectedByDefault_whenThirdItemIsSelected_thenSelectedValueDisplayed() {


    composeTestRule.apply {


    setContent { Demo_RadioButton () }


    onNodeWithRoleAndText(Role.RadioButton, "Item 1")


    .assertIsSelected()


    onNodeWithText("Item 3")


    .performClick()


    onNodeWithRoleAndText(Role.RadioButton, "Item 3")


    .assertIsSelected()


    onNodeWithText("Selected value: Item 3")


    .assertIsDisplayed()


    }


    }


    private fun withRole(role: Role): SemanticsMatcher {


    return SemanticsMatcher("${SemanticsProperties.Role.name} contains '$role'") {


    it.config.getOrNull(SemanticsProperties.Role) == role


    }


    }


    private fun SemanticsNodeInteractionsProvider.onNodeWithRoleAndText(


    role: Role,


    text: String


    ) = onNode(


    withRole(role)


    .and(isSelectable())


    .and(isEnabled())


    .and(hasText(text))


    )


    }


    View Slide

  40. Application

    View Slide

  41. View Slide

  42. Presentation (UI)
    Data layer
    *Repository SessionManager
    SharedPreferences
    SharedPreferences Database

    View Slide

  43. End-To-End tests

    View Slide

  44. View Slide

  45. Database

    View Slide

  46. View Slide

  47. Session Manager Shared Preferences

    View Slide

  48. Functional tests

    View Slide

  49. View Slide

  50. launchFragmentInContainer(


    themeResId = R.style.Theme_MoodTracker


    )


    composeTestRule.apply {


    ...


    }


    View Slide

  51. Server
    Database

    View Slide

  52. Database
    Query
    Dependencies
    UI interaction

    View Slide

  53. Prod server
    Dev server
    Mock/Fake

    View Slide

  54. Prod server
    Dev server DATA SYNCHRONIZATION
    We need to synchronize data between
    the production and dev servers.
    INTERACTION WITH THE SERVER
    We make requests to the production
    server. The connection can require
    certificates, VPN, etc.
    PRODUCTION BACK-END
    We always use the latest available
    production environment.

    View Slide

  55. Mock/Fake
    DATA SYNCHRONIZATION
    We need to synchronize predefined
    responses with responses from the
    production server.
    NO INTERACTION WITH THE SERVER
    Responses from the production server
    can differ from the predefined data.
    We can use predefined fake responses
    instead of calling the production
    server.
    MAKE TESTS MORE STABLE

    View Slide

  56. Mock/Fake

    View Slide

  57. Mock/Fake

    View Slide

  58. END-TO-END (E2E) TESTS FUNCTIONAL TESTS
    ENTRY POINT


    Similar to the entry point of an
    app.
    APP VERIFICATION


    Slow veri
    fi
    cation of the app.
    NAVIGATION


    Navigate to the required
    screen.
    SERVER INTERACTION


    Interaction with the production
    server.
    ENTRY POINT


    Start the required screen of the
    app.
    SERVER INTERACTION


    Usually interaction with non-
    production server.
    NAVIGATION


    Usually no navigation.
    UI VERIFICATION


    Fast veri
    fi
    cation of components
    or screens.

    View Slide

  59. Screenshot tests

    View Slide

  60. View Slide

  61. What can we check with screenshot tests?

    View Slide

  62. @Composable


    fun LoadingButton(


    onClick: () -> Unit,


    text: String,


    modifier: Modifier = Modifier,


    shape: Shape = MaterialTheme.shapes.small,


    enabled: Boolean = true,


    isLoading: Boolean = false,


    loadingIndicatorColor: Color = MaterialTheme.colorScheme.surface


    ) {


    ...


    }

    View Slide

  63. AndroidUiTestingUtils
    https://github.com/sergio-sastre/AndroidUiTestingUtils
    Locale Light/Dark


    mode
    Display size,


    screen orientation
    Font size

    View Slide

  64. View Slide

  65. View Slide

  66. View Slide

  67. View Slide

  68. @Test


    fun settingsScreen_dark_defaultState() {


    val fragmentScenario = FragmentScenarioConfigurator


    .setUiMode(UiMode.NIGHT)


    .setTheme(R.style.Theme_MoodTracker)


    .launchInContainer(


    SettingsFragment::class.java


    )


    compareScreenshot(


    fragment = fragmentScenario.waitForFragment(),


    name = "settingsScreen_dark"


    )


    fragmentScenario.close()


    }


    @Test


    fun settingsScreen_light_defaultState() {


    val fragmentScenario = FragmentScenarioConfigurator


    .setUiMode(UiMode.DAY)


    .setTheme(R.style.Theme_MoodTracker)


    .launchInContainer(


    SettingsFragment::class.java


    )


    compareScreenshot(


    fragment = fragmentScenario.waitForFragment(),


    name = "settingsScreen_light"


    )


    fragmentScenario.close()


    }


    View Slide

  69. What to test?

    View Slide

  70. PIXEL PERFECTNESS INTERACTION
    TESTING UI COMPONENTS

    (DESIGN SYSTEM)
    INTERACTION WITH SCREEN(S)
    SUPPORT MULTIPLE LANGUAGES
    SUPPORT DIFFERENT

    COLOR THEMES
    SUPPORT ACCESSIBILITY OPTIONS

    (FONT SIZE, SCREEN SIZE, ETC)
    SUPPORT LTR & RTL
    PERMISSION TESTING
    YES
    YES YES
    YES
    YES
    YES
    YES
    YES

    View Slide

  71. END-TO-END TESTS
    FUNCTIONAL TESTS
    SCREENSHOT TESTS

    View Slide

  72. End To End testing

    Maestro

    View Slide

  73. View Slide

  74. •add-emotional-state.yaml


    •login-test-user.yaml


    •time-format-flow.yaml
    TA
    SK
    e2e-flow-test

    View Slide

  75. appId: com.alexzh.moodtracker


    ---


    - launchApp:


    clearState: true


    - tapOn: "Add"


    - tapOn: "Excited"


    - tapOn: "Select a time"


    - tapOn: "8"


    - tapOn: "15"


    - tapOn: "OK"


    - tapOn: "Gaming"


    - tapOn: "Save"


    - assertVisible: "08:15"


    - assertVisible: "Gaming"


    @Test
    fun displayEmotion_WhenEmotionHistoryWasAddedViaAddMoodScreen() {
    ActivityScenario.launch(HomeActivity::class.java)
    composableTestRule.apply {
    waitUntil {
    onAllNodesWithText("Emotions")
    .fetchSemanticsNodes().size == 1
    }
    onNode(hasContentDescription("Add"))
    .performClick()
    waitUntil {
    onAllNodesWithContentDescription("Happy")
    .fetchSemanticsNodes().size == 1
    }
    onNodeWithContentDescription("Happy")
    .performClick()
    onNodeWithText("Reading")
    .performClick()
    onNodeWithText("Gaming")
    .performClick()
    onNodeWithText("Note")
    .performTextInput("Test note")
    onNode(hasText("Save"))
    .performScrollTo()
    .performClick()
    waitUntil {
    onAllNodesWithText("Emotions")
    .fetchSemanticsNodes().size == 1
    }
    onNode(withEmotionStateAndNote("Happy", "Test note"))
    .assert(hasAnyChild(hasText("Gaming")))
    onNode(hasContentDescription("Happy"))
    .performSemanticsAction(SemanticsActions.OnClick)
    }
    }

    View Slide

  76. appId: com.alexzh.moodtracker


    ---


    - launchApp:


    clearState: true


    - tapOn: "Add"


    - tapOn: "Excited"


    - tapOn: "Select a time"


    - tapOn: "8"


    - tapOn: "15"


    - tapOn: "OK"


    - tapOn: "Gaming"


    - tapOn: "Save"


    - assertVisible: "08:15"


    - assertVisible: "Gaming"


    launchApp()


    todayScreen {


    addEmotionalState()


    }


    addMoodScreen {


    selectEmotion("Happy")


    selectActivity("Reading", "Gaming")


    enterNote(note)


    save()


    }


    todayScreen {


    hasItem(


    "Happy",


    “Test note”,


    "Reading", "Gaming"


    )


    openEmotionalItem("Happy")


    }


    addMoodScreen {


    delete()


    }


    View Slide

  77. @Test


    fun displayEmotion_WhenEmotionHistoryWasAddedViaAddMoodScreen() {


    ActivityScenario.launch(HomeActivity::class.java)


    composableTestRule.apply {


    waitUntil {


    onAllNodesWithText("Emotions")


    .fetchSemanticsNodes().size == 1


    }


    onNode(hasContentDescription("Add"))


    .performClick()


    waitUntil {


    onAllNodesWithContentDescription("Happy")


    .fetchSemanticsNodes().size == 1


    }


    onNodeWithContentDescription("Happy")


    .performClick()


    onNodeWithText("Reading")


    .performClick()


    onNodeWithText("Gaming")


    .performClick()


    onNodeWithText("Note")


    .performTextInput("Test note")


    onNode(hasText("Save"))


    .performScrollTo()


    .performClick()


    waitUntil {


    onAllNodesWithText("Emotions")


    .fetchSemanticsNodes().size == 1


    }


    onNode(withEmotionStateAndNote("Happy", "Test note"))


    .assert(hasAnyChild(hasText("Gaming")))


    onNode(hasContentDescription("Happy"))


    .performSemanticsAction(SemanticsActions.OnClick)


    }


    }
    launchApp()
    todayScreen {
    addEmotionalState()
    }
    addMoodScreen {
    selectEmotion("Happy")
    selectActivity("Reading", "Gaming")
    enterNote(note)
    save()
    }
    todayScreen {
    hasItem(
    "Happy",
    note,
    "Reading", "Gaming"
    )
    openEmotionalItem("Happy")
    }
    addMoodScreen {
    delete()
    }

    View Slide

  78. Screenshot tests

    View Slide

  79. View Slide

  80. https://github.com/cashapp/paparazzi
    https://github.com/pedrovgs/Shot
    SHOT PAPARAZZI

    View Slide

  81. SHOT PAPARAZZI
    https://github.com/cashapp/paparazzi
    https://github.com/pedrovgs/Shot
    DEVICE JVM

    View Slide

  82. https://github.com/cashapp/paparazzi
    https://github.com/pedrovgs/Shot
    SHOT
    DEVICE
    PAPARAZZI
    JVM

    View Slide

  83. Testing Components

    in isolation

    View Slide

  84. View Slide

  85. TA
    SK
    LoadingButtonColorModeScreenshotTest
    •loadingButton_light_defaultState


    •loadingButton_dark_defaultState


    •loadingButton_light_loadingState


    •loadingButton_dark_loadingState


    •loadingButton_loadingAnimation (LoadingButtonAnimationTest)

    View Slide

  86. TA
    SK
    WeekCalendarColorModeScreenshotTest
    •weekCalendar_light_todayInSelectedDate


    •weekCalendar_dark_todayInSelectedDate


    •weekCalendar_light_todayInNotSelectedDate


    •weekCalendar_dark_todayInNotSelectedDate

    View Slide

  87. Testing Screens

    in isolation

    View Slide

  88. TA
    SK
    SettingsScreenScreenshotTest
    •settingsScreen_light_defaultState


    •settingsScreen_dark_defaultState

    View Slide

  89. Parameterized tests

    View Slide

  90. TestParameterInjector
    https://github.com/google/TestParameterInjector
    Strings Enums
    Primitive Types

    View Slide

  91. View Slide

  92. TA
    SK
    LoadingButtonParamScreenshotTest
    •loadingButton_customUiModeAndLoadingStateAndTitle

    View Slide

  93. TA
    SK
    WeekCalendarParamScreenshotTest
    •weekCalendar_paramUiModeAndSelectedDate

    View Slide

  94. TA
    SK
    SettingsScreenParamScreenshotTest
    •settingsScreen_customFontSizeAnUiMode

    View Slide

  95. Functional testing

    View Slide

  96. END-TO-END (E2E) TESTS FUNCTIONAL TESTS
    ENTRY POINT


    Similar to the entry point of an
    app.
    APP VERIFICATION


    Slow veri
    fi
    cation of the app.
    NAVIGATION


    Navigate to the required
    screen.
    SERVER INTERACTION


    Interaction with the production
    server.
    ENTRY POINT


    Start the required screen of the
    app.
    SERVER INTERACTION


    Usually interaction with non-
    production server.
    NAVIGATION


    Usually no navigation.
    UI VERIFICATION


    Fast veri
    fi
    cation of components
    or screens.

    View Slide

  97. TA
    SK
    ProfileScreenTest
    •displayUserInfo_WhenUserIsLoggedIn


    •displayCreateAccountAndLoginOptions_WhenUserIsNotLoggedIn

    View Slide

  98. TA
    SK
    LoginScreenTest
    •displayEmailIsTooShortError_whenEnteredEmailIsShorterThanFourSymbols


    •displayPasswordIsTooShortError_whenEnteredPasswordIsShorterThanFourSymbols

    View Slide

  99. TA
    SK
    TodayScreenTest
    •displaySuccessWithSimpleItem_whenDataIsAvailable


    •displaySuccessWithMultipleItems_whenDataIsAvailable


    •displayEmptyState_whenDataIsNotAvailable

    View Slide

  100. Readability

    View Slide

  101. onNodeWithText("Login")


    .performClick()


    onNodeWithText("Email")


    .performTextInput("[email protected]")


    onNodeWithText("Password")


    .performTextInput("password")


    onNode(hasText("LOGIN"))


    .performClick()




    onNodeWithText("Alex")


    .assertIsDisplayed()


    onNodeWithText(“[email protected]")


    .assertIsDisplayed()


    View Slide

  102. // Profile screen


    onNodeWithText("Login")


    .performClick()


    // Login screen


    onNodeWithText("Email")


    .performTextInput("[email protected]")


    onNodeWithText("Password")


    .performTextInput("password")


    onNode(hasText("LOGIN"))


    .performClick()


    // Profile screen


    onNodeWithText("Alex")


    .assertIsDisplayed()


    onNodeWithText("[email protected]")


    .assertIsDisplayed()


    View Slide

  103. profileScreen(composeTestRule) {


    tapOnLogin()


    }


    loginScreen(composeTestRule) {


    login(


    email = "[email protected]",


    password = "password"


    )


    }


    profileScreen(composeTestRule) {


    hasUserInfo(


    email = "[email protected]",


    name = "Alex"


    )


    }

    View Slide

  104. BASIC OPERATIONS
    SCREEN ROBOTS
    TEST CASES
    @RunWith(AndroidJUnit4::class)


    class LoginActivityTest {


    @Test


    fun shouldBeDisplayedEmptyEmailErrorsWhenValueIsEmpty() {


    loginScreen {


    enterEmail(EMPTY_VALUE)


    emptyEmailErrorDisplayed()


    }


    }


    @Test


    fun shouldBeDisplayedEmailErrorsWhenValueIsNotEmail() {


    loginScreen {


    enterEmail(INCORRECT_EMAIL)


    incorrectEmailErrorDisplayed()


    }


    }


    @Test


    fun shouldBeDisplayedEmptyPasswordErrorsWhenValueIsEmpty() {


    loginScreen {


    enterPassword(EMPTY_VALUE)


    emptyPasswordErrorDisplayed()


    }


    }


    ...


    }
    class LoginScreenRobot : BaseTestRobot() {


    fun enterEmail(email: String) =


    enterText(R.id.email, email)




    fun enterPassword(email: String) =

    enterText(R.id.password, password)


    ...


    }
    open class BaseTestRobot {


    fun enterText(viewId: Int, text: String) {


    onView(withId(viewId))


    .perform(replaceText(text))


    }


    fun clickOnView(viewId: Int) {


    onView(withId(viewId))


    .perform(click())


    }



    ...


    }
    ESPRESSO

    View Slide

  105. BASIC OPERATIONS
    SCREEN ROBOTS
    TEST CASES
    @RunWith(AndroidJUnit4::class)


    class LoginActivityTest {


    @Test


    fun shouldBeDisplayedEmptyEmailErrorsWhenValueIsEmpty() {


    loginScreen {


    enterEmail(EMPTY_VALUE)


    emptyEmailErrorDisplayed()


    }


    }


    @Test


    fun shouldBeDisplayedEmailErrorsWhenValueIsNotEmail() {


    loginScreen {


    enterEmail(INCORRECT_EMAIL)


    incorrectEmailErrorDisplayed()


    }


    }


    @Test


    fun shouldBeDisplayedEmptyPasswordErrorsWhenValueIsEmpty() {


    loginScreen {


    enterPassword(EMPTY_VALUE)


    emptyPasswordErrorDisplayed()


    }


    }


    ...


    }
    class LoginScreenRobot : BaseTestRobot() {


    fun enterEmail(email: String) =


    enterText(R.id.email, email)




    fun enterPassword(email: String) =

    enterText(R.id.password, password)


    ...


    }
    open class BaseTestRobot {


    fun enterText(viewId: Int, text: String) {


    val view = device.findObject(By.res(resId(viewId)))


    view.text = text


    }


    fun clickOnView(viewId: Int) {


    device.findObject(By.res(resIf(viewId)))

    .click()


    }



    ...


    }
    UI AUTOMATOR

    View Slide

  106. open class BaseTestRobot {


    @get:Rule val composeTestRule = createComposeRule()




    fun enterText(tag: String, text: String) {


    composeTestRule.onNodeWithTag(tag)


    .performTextInput(text)


    }


    fun clickOnView(tag: String) {


    composeTestRule.onNodeWithTag(tag)


    .performClick()


    }


    }
    @RunWith(AndroidJUnit4::class)


    class LoginActivityTest {


    @Test


    fun shouldBeDisplayedEmptyEmailErrorsWhenValueIsEmpty() {


    loginScreen {


    enterEmail(EMPTY_VALUE)


    emptyEmailErrorDisplayed()


    }


    }


    @Test


    fun shouldBeDisplayedEmailErrorsWhenValueIsNotEmail() {


    loginScreen {


    enterEmail(INCORRECT_EMAIL)


    incorrectEmailErrorDisplayed()


    }


    }


    @Test


    fun shouldBeDisplayedEmptyPasswordErrorsWhenValueIsEmpty() {


    loginScreen {


    enterPassword(EMPTY_VALUE)


    emptyPasswordErrorDisplayed()


    }


    }


    ...


    }
    BASIC OPERATIONS
    SCREEN ROBOTS
    TEST CASES
    COMPOSE UI TESTS
    class LoginScreenRobot : BaseTestRobot() {


    fun enterEmail(email: String) =


    enterText("email", email)




    fun enterPassword(email: String) =

    enterText("password", password)


    ...


    }

    View Slide

  107. TA
    SK
    TodayScreenE2ETestDsl
    •displayEmotion_WhenEmotionHistoryWasAddedViaAddMoodScreen

    View Slide

  108. Flaky Tests

    View Slide

  109. External dependencies
    Framework
    Test case execution
    Device and emulator

    View Slide

  110. External dependencies
    - Network connection (VPN)


    - Network speed


    - Back-end

    View Slide

  111. Framework
    - Framework issues


    - Toast, Snackbar, etc


    - Custom Views

    View Slide

  112. Device and emulator
    - Performance


    - Noti
    fi
    cations


    - Device memory

    View Slide

  113. Test case execution
    - Simulate User actions


    - Incorrect state before/after
    a test case


    - Toast, Snackbar, etc

    View Slide

  114. - Network connection (VPN)


    - Network speed


    - Back-end
    - Framework issues


    - Toast, Snackbar, etc


    - Custom Views
    - Simulate User actions


    - Incorrect state before/after a
    test case


    - Toast, Snackbar, etc
    - Performance


    - Noti
    fi
    cations


    - Device memory
    External dependencies
    Framework
    Test case execution
    Device and emulator

    View Slide

  115. ?
    ?
    ?

    View Slide

  116. #ExploreMore


    UI Testing
    @alex_zhukovich https://alexzh.com/

    View Slide