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

Efficient UI testing in Android

Efficient UI testing in Android

Mobile apps are growing. They become more complex and require more testing. It means that it is time to integrate fast and stable automated tests into your project.

In this talk, we will discuss the following topics:
- How to create fast and stable 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

Alex Zhukovich

August 01, 2022
Tweet

More Decks by Alex Zhukovich

Other Decks in Programming

Transcript

  1. @alex_zhukovich
    https://alexzh.com/
    Ef
    fi
    cient

    UI Testing

    View Slide

  2. View Slide

  3. View Slide

  4. View Slide

  5. View Slide

  6. Exploring the app

    View Slide

  7. ACTIVITY FRAGMENT

    View Slide

  8. ACTIVITY FRAGMENT
    COMPOSABLE
    FUNCTION

    View Slide

  9. launchActivity
    launchFragmentInContainer
    ComposeContentTestRule
    ActivityScenario.launch(


    MainActivity::class.java


    )


    View Slide

  10. 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(


    View Slide

  11. 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.1"

    View Slide

  12. launchActivity
    launchFragmentInContainer
    ComposeContentTestRule
    Activity
    Fragment

    View Slide

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

    View Slide

  14. 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:1:1"

    View Slide

  15. launchActivity
    launchFragmentInContainer
    ComposeContentTestRule
    Component
    Activity
    @Composable

    View Slide

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

    View Slide

  17. End-To-End testing

    View Slide

  18. View Slide

  19. View Slide

  20. END-TO-END TESTS
    class ProfileE2ETest {


    @get:Rule


    val composableTestRule = createAndroidComposeRule()


    fun hasText(@StringRes resId: Int) =


    hasText(composableTestRule.activity.getString(resId))




    @Test


    fun createAccount_emptyNameErrorMessage() {


    composableTestRule.apply {


    onNode(hasText(R.string.navigation_settings_label))


    .performClick()


    onNode(hasText(R.string.settingsScreen_profile_title))


    .performClick()


    onNode(hasText(R.string.profileScreen_createAccount_button))


    .performClick()


    onNode(hasText(R.string.createAccountScreen_createAccount_button))


    .performClick()


    onNode(hasText(R.string.createAccountScreen_error_nameIsTooShort_label))


    .assertIsDisplayed()


    }


    }


    }


    View Slide

  21. END-TO-END TESTS
    class ProfileE2ETest {


    @get:Rule


    val composableTestRule = createAndroidComposeRule()


    fun hasText(@StringRes resId: Int) =


    hasText(composableTestRule.activity.getString(resId))




    @Test


    fun createAccount_emptyNameErrorMessage() {


    composableTestRule.apply {


    onNode(hasText(R.string.navigation_settings_label))


    .performClick()


    onNode(hasText(R.string.settingsScreen_profile_title))


    .performClick()


    onNode(hasText(R.string.profileScreen_createAccount_button))


    .performClick()


    onNode(hasText(R.string.createAccountScreen_createAccount_button))


    .performClick()


    onNode(hasText(R.string.createAccountScreen_error_nameIsTooShort_label))


    .assertIsDisplayed()


    }


    }


    }


    View Slide

  22. END-TO-END TESTS
    class ProfileE2ETest {


    @get:Rule


    val composableTestRule = createAndroidComposeRule()


    fun hasText(@StringRes resId: Int) =


    hasText(composableTestRule.activity.getString(resId))




    @Test


    fun createAccount_emptyNameErrorMessage() {


    composableTestRule.apply {


    onNode(hasText(R.string.navigation_settings_label))


    .performClick()


    onNode(hasText(R.string.settingsScreen_profile_title))


    .performClick()


    onNode(hasText(R.string.profileScreen_createAccount_button))


    .performClick()


    onNode(hasText(R.string.createAccountScreen_createAccount_button))


    .performClick()


    onNode(hasText(R.string.createAccountScreen_error_nameIsTooShort_label))


    .assertIsDisplayed()


    }


    }


    }


    NAVIGATION ≈ 3 SEC

    View Slide

  23. END-TO-END TESTS
    NAVIGATION
    Navigate to the required screen
    INTERACTION WITH THE SERVER
    Interaction with the production
    server
    ENTRY POINT
    Similar to the entry point of an app
    APP VERIFICATION
    Slow verification of the app

    View Slide

  24. Functional testing

    View Slide

  25. FUNCTIONAL TESTS
    class LoginScreenTest : KoinTest {


    @get:Rule


    val composableTestRule = createAndroidComposeRule()


    @Before


    fun setup() {


    stopKoin()


    startKoin {


    androidContext(InstrumentationRegistry.getInstrumentation().targetContext)


    modules(dataModule, appModule)


    }


    }


    @Test


    fun createAccount_emptyNameErrorMessage() {


    composableTestRule.apply {


    setContent {


    CreateAccountScreen(


    viewModel = get(),


    onLogin = { },


    onCreateAccountSuccess = { }

    )


    }


    onNode(hasText(R.string.createAccountScreen_createAccount_button))


    .performClick()


    onNode(hasText(R.string.createAccountScreen_error_nameIsTooShort_label))


    .assertIsDisplayed()


    }


    }


    fun hasText(@StringRes resId: Int) =


    hasText(composableTestRule.activity.getString(resId))


    }


    View Slide

  26. View Slide

  27. FUNCTIONAL TESTS
    NAVIGATION
    Usually no navigation
    INTERACTION WITH THE SERVER
    Usually interaction with non-
    production server
    ENTRY POINT
    Start the required screen of the app
    APP VERIFICATION
    Fast verification of components or
    screens

    View Slide

  28. Screenshot testing

    View Slide

  29. INTERACTION
    PIXEL PERFECTNESS

    View Slide

  30. INTERACTION
    PIXEL PERFECTNESS

    View Slide

  31. View Slide

  32. View Slide

  33. class WeekCalendarTest : ScreenshotTest {


    private val testDate = LocalDate.of(2022, 5, 5)


    @get:Rule


    val composeTestRule = createComposeRule()


    @Test


    fun weekCalendar_todayIsSelectedDate() {


    composeTestRule.setContent {


    AppTheme(darkTheme = theme == Theme.DARK) {


    WeekCalendar(


    startDate = testDate.minusDays(6),


    selectedDate = testDate.minusDays(1),


    onSelectedDateChange = {},


    todayDate = testDate.minusDays(1)


    )


    }


    }




    compareScreenshot(composeTestRule)


    }


    }


    View Slide

  34. Ef
    fi
    cient UI testing

    View Slide

  35. END-TO-END TESTS
    FUNCTIONAL TESTS
    SCREENSHOT TESTS

    View Slide

  36. FUNCTIONAL TESTS
    SCREENSHOT TESTS

    View Slide

  37. END-TO-END TESTS SCREENSHOT TESTS

    View Slide

  38. END-TO-END TESTS
    FUNCTIONAL TESTS

    View Slide

  39. END-TO-END TESTS
    FUNCTIONAL TESTS
    SCREENSHOT TESTS

    View Slide

  40. Lesson learned

    View Slide

  41. READABILITY

    View Slide

  42. onNodeWithText("Login")


    .performClick()


    onNodeWithText("Email")


    .performTextInput(email)


    onNodeWithText("Password")


    .performTextInput(password)


    onNode(hasText("LOGIN"))


    .performClick()


    onNodeWithText(email)


    .assertIsDisplayed()


    onNodeWithText(username)


    .assertIsDisplayed()


    View Slide

  43. // Profile screen


    onNodeWithText("Login")


    .performClick()


    // Login screen


    onNodeWithText("Email")


    .performTextInput(email)


    onNodeWithText("Password")


    .performTextInput(password)


    onNode(hasText("LOGIN"))


    .performClick()


    // Profile screen


    onNodeWithText(email)


    .assertIsDisplayed()


    onNodeWithText(username)


    .assertIsDisplayed()


    View Slide

  44. profileScreen {


    launch()


    hasTitle()


    tapOnLogin()


    }


    loginScreen {


    login(email, password)


    }


    profileScreen {


    hasUserInfo(email, username)


    }


    View Slide

  45. FLAKY TESTS
    READABILITY

    View Slide

  46. External dependencies
    Framework
    Test case execution
    Device and emulator

    View Slide

  47. External dependencies
    - Network connection (VPN)


    - Network speed


    - Back-end

    View Slide

  48. Framework
    - Framework issues


    - Toast, Snackbar, etc


    - Custom Views

    View Slide

  49. Device and emulator
    - Performance


    - Noti
    fi
    cations


    - Device memory

    View Slide

  50. Test case execution
    - Simulate User actions


    - Incorrect state before/after
    a test case


    - Toast, Snackbar, etc

    View Slide

  51. - 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

  52. LEARN YOUR TOOLS
    FLAKY TESTS
    READABILITY

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  56. LEARN YOUR TOOLS
    PARAMETERIZED TESTS
    FLAKY TESTS
    READABILITY

    View Slide

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


    mode
    Display size,


    screen orientation
    Font size

    View Slide

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

    View Slide

  59. @RunWith(TestParameterInjector::class)


    class SettingsScreenParamScreenshotTest : ScreenshotTest {


    @get:Rule


    val composeTestRule = createEmptyComposeRule()


    @Test


    fun settingsScreen_customFontSizeAndUiMode(


    @TestParameter fontSize: FontSize,


    @TestParameter uiMode: UiMode


    ) {


    val activityScenario = ActivityScenarioConfigurator.ForComposable()


    .setFontSize(fontSize)


    .setUiMode(uiMode)


    .launchConfiguredActivity()


    .onActivity {


    it.setContent {


    AppTheme {


    SettingsScreen(


    onProfile = {},


    onDocs = {}


    )


    }


    }


    }


    activityScenario.waitForActivity()


    compareScreenshot(composeTestRule, "settingsScreen_${uiMode}_${fontSize}_defaultState")


    activityScenario.close()


    }


    }

    View Slide

  60. @RunWith(TestParameterInjector::class)


    class SettingsScreenParamScreenshotTest : ScreenshotTest {


    @get:Rule


    val composeTestRule = createEmptyComposeRule()


    @Test


    fun settingsScreen_customFontSizeAndUiMode(


    @TestParameter fontSize: FontSize,


    @TestParameter uiMode: UiMode


    ) {


    val activityScenario = ActivityScenarioConfigurator.ForComposable()


    .setFontSize(fontSize)


    .setUiMode(uiMode)


    .launchConfiguredActivity()


    .onActivity {


    it.setContent {


    AppTheme {


    SettingsScreen(


    onProfile = {},


    onDocs = {}


    )


    }


    }


    }


    activityScenario.waitForActivity()


    compareScreenshot(composeTestRule, "settingsScreen_${uiMode}_${fontSize}_defaultState")


    activityScenario.close()


    }


    }

    View Slide

  61. View Slide

  62. DIFFERENT APPROACHES
    LEARN YOUR TOOLS
    PARAMETERIZED TESTS
    FLAKY TESTS
    READABILITY

    View Slide

  63. Server
    Database
    PRE-POPULATING THE DATABASE

    View Slide

  64. Database
    DATA LAYER
    Query
    Dependencies
    UI interaction
    PRE-POPULATING THE DATABASE
    TEST CASE

    View Slide

  65. Prod
    server
    Dev server
    Mock/Fake
    REAL DATA VS FAKE DATA

    View Slide

  66. 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.
    REAL DATA VS FAKE DATA
    Prod
    server
    Dev server

    View Slide

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

    View Slide

  68. 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

  69. Next steps

    View Slide

  70. GROUP TEST CASES TESTING WIDGETS TESTING SHORTCUTS

    View Slide

  71. #ExploreMore


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

    View Slide