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 full-size slide

  2. Android Testing World

    View full-size slide

  3. ANDROID TESTS
    LOCAL INSTRUMENTATION

    View full-size slide

  4. ANDROID TESTS
    LOCAL INSTRUMENTATION
    UI* Non-UI

    View full-size slide

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

    View full-size slide

  6. app.apk
    tests.apk
    BUILD APKs

    View full-size slide

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


    am instrument

    View full-size slide

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


    am instrument

    View full-size slide

  9. ACTIVITY FRAGMENT

    View full-size slide

  10. ACTIVITY FRAGMENT
    COMPOSABLE
    FUNCTION

    View full-size slide

  11. launchActivity
    launchFragmentInContainer
    ComposeContentTestRule
    ActivityScenario.launch(


    MainActivity::class.java


    )


    View full-size slide

  12. 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 full-size slide

  13. 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 full-size slide

  14. launchActivity
    launchFragmentInContainer
    ComposeContentTestRule
    Activity
    Fragment

    View full-size slide

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

    View full-size slide

  16. 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 full-size slide

  17. launchActivity
    launchFragmentInContainer
    ComposeContentTestRule
    Component
    Activity
    @Composable

    View full-size slide

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

    View full-size slide

  19. Testing Android Views

    View full-size slide

  20. COMPONENT HAS ID PROPERTIES REAL CLOCK

    View full-size slide

  21. 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 full-size slide

  22. Testing Composable code

    View full-size slide

  23. NO ID

    OF COMPONENT
    SEMANTIC
    PROPERTIES
    VIRTUAL CLOCK

    View full-size slide

  24. 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 full-size slide

  25. 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 full-size slide

  26. 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 full-size 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()


    }


    }


    }


    Finder Matcher
    Action
    Assertion

    View full-size slide

  28. 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 full-size slide

  29. 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 full-size slide

  30. 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 full-size slide

  31. onAllNodesWithContentDescription("Happy")[0]


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


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


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


    onNodeWithText("Add")


    .performClick()

    View full-size slide

  32. @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 full-size slide

  33. @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 full-size slide

  34. @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 full-size slide

  35. @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 full-size slide

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

    View full-size slide

  37. End-To-End tests

    View full-size slide

  38. Session Manager Shared Preferences

    View full-size slide

  39. Functional tests

    View full-size slide

  40. launchFragmentInContainer(


    themeResId = R.style.Theme_MoodTracker


    )


    composeTestRule.apply {


    ...


    }


    View full-size slide

  41. Server
    Database

    View full-size slide

  42. Database
    Query
    Dependencies
    UI interaction

    View full-size slide

  43. Prod server
    Dev server
    Mock/Fake

    View full-size slide

  44. 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 full-size slide

  45. 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 full-size slide

  46. 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 full-size slide

  47. Screenshot tests

    View full-size slide

  48. What can we check with screenshot tests?

    View full-size slide

  49. @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 full-size slide

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


    mode
    Display size,


    screen orientation
    Font size

    View full-size slide

  51. @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 full-size slide

  52. What to test?

    View full-size slide

  53. 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 full-size slide

  54. END-TO-END TESTS
    FUNCTIONAL TESTS
    SCREENSHOT TESTS

    View full-size slide

  55. End To End testing

    Maestro

    View full-size slide

  56. •add-emotional-state.yaml


    •login-test-user.yaml


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

    View full-size slide

  57. 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 full-size slide

  58. 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 full-size slide

  59. @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 full-size slide

  60. Screenshot tests

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  64. Testing Components

    in isolation

    View full-size slide

  65. TA
    SK
    LoadingButtonColorModeScreenshotTest
    •loadingButton_light_defaultState


    •loadingButton_dark_defaultState


    •loadingButton_light_loadingState


    •loadingButton_dark_loadingState


    •loadingButton_loadingAnimation (LoadingButtonAnimationTest)

    View full-size slide

  66. TA
    SK
    WeekCalendarColorModeScreenshotTest
    •weekCalendar_light_todayInSelectedDate


    •weekCalendar_dark_todayInSelectedDate


    •weekCalendar_light_todayInNotSelectedDate


    •weekCalendar_dark_todayInNotSelectedDate

    View full-size slide

  67. Testing Screens

    in isolation

    View full-size slide

  68. TA
    SK
    SettingsScreenScreenshotTest
    •settingsScreen_light_defaultState


    •settingsScreen_dark_defaultState

    View full-size slide

  69. Parameterized tests

    View full-size slide

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

    View full-size slide

  71. TA
    SK
    LoadingButtonParamScreenshotTest
    •loadingButton_customUiModeAndLoadingStateAndTitle

    View full-size slide

  72. TA
    SK
    WeekCalendarParamScreenshotTest
    •weekCalendar_paramUiModeAndSelectedDate

    View full-size slide

  73. TA
    SK
    SettingsScreenParamScreenshotTest
    •settingsScreen_customFontSizeAnUiMode

    View full-size slide

  74. Functional testing

    View full-size slide

  75. 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 full-size slide

  76. TA
    SK
    ProfileScreenTest
    •displayUserInfo_WhenUserIsLoggedIn


    •displayCreateAccountAndLoginOptions_WhenUserIsNotLoggedIn

    View full-size slide

  77. TA
    SK
    LoginScreenTest
    •displayEmailIsTooShortError_whenEnteredEmailIsShorterThanFourSymbols


    •displayPasswordIsTooShortError_whenEnteredPasswordIsShorterThanFourSymbols

    View full-size slide

  78. TA
    SK
    TodayScreenTest
    •displaySuccessWithSimpleItem_whenDataIsAvailable


    •displaySuccessWithMultipleItems_whenDataIsAvailable


    •displayEmptyState_whenDataIsNotAvailable

    View full-size slide

  79. onNodeWithText("Login")


    .performClick()


    onNodeWithText("Email")


    .performTextInput("[email protected]")


    onNodeWithText("Password")


    .performTextInput("password")


    onNode(hasText("LOGIN"))


    .performClick()




    onNodeWithText("Alex")


    .assertIsDisplayed()


    onNodeWithText(“[email protected]")


    .assertIsDisplayed()


    View full-size slide

  80. // 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 full-size slide

  81. profileScreen(composeTestRule) {


    tapOnLogin()


    }


    loginScreen(composeTestRule) {


    login(


    email = "[email protected]",


    password = "password"


    )


    }


    profileScreen(composeTestRule) {


    hasUserInfo(


    email = "[email protected]",


    name = "Alex"


    )


    }

    View full-size slide

  82. 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 full-size slide

  83. 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 full-size slide

  84. 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 full-size slide

  85. TA
    SK
    TodayScreenE2ETestDsl
    •displayEmotion_WhenEmotionHistoryWasAddedViaAddMoodScreen

    View full-size slide

  86. External dependencies
    Framework
    Test case execution
    Device and emulator

    View full-size slide

  87. External dependencies
    - Network connection (VPN)


    - Network speed


    - Back-end

    View full-size slide

  88. Framework
    - Framework issues


    - Toast, Snackbar, etc


    - Custom Views

    View full-size slide

  89. Device and emulator
    - Performance


    - Noti
    fi
    cations


    - Device memory

    View full-size slide

  90. Test case execution
    - Simulate User actions


    - Incorrect state before/after
    a test case


    - Toast, Snackbar, etc

    View full-size slide

  91. - 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 full-size slide

  92. #ExploreMore


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

    View full-size slide