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

Efficient Android UI Testing, Droidcon Italy 2021

Efficient Android UI Testing, Droidcon Italy 2021

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

November 12, 2021
Tweet

More Decks by Alex Zhukovich

Other Decks in Technology

Transcript

  1. androidTest test commonTest android { ... sourceSets { androidTest {

    java.srcDirs += "src/commonTest/java" } test { java.srcDirs += "src/commonTest/java" } } }
  2. 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(
  3. 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-rc01"
  4. 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"
  5. onView(withId(R.id.emailEditText)) .perform(replaceText(email)) onView(withId(R.id.passwordEditText)) .perform(replaceText(password), closeSoftKeyboard()) onView(withId(R.id.loginButton)) .perform(click()) val progressBarIR =

    ViewVisibilityIdlingResource(...) IdlingRegistry.getInstance().register(progressBarIR) onView(withId(R.id.recyclerView)) .check(matches(withItemCount(13))) IdlingRegistry.getInstance().unregister(progressBarIR) onView(withId(R.id.navigation_profile)) .perform(click()) ...
  6. // Login screen onView(withId(R.id.emailEditText)) .perform(replaceText(email)) onView(withId(R.id.passwordEditText)) .perform(replaceText(password), closeSoftKeyboard()) onView(withId(R.id.loginButton)) .perform(click())

    // Home screen val progressBarIR = ViewVisibilityIdlingResource(...) IdlingRegistry.getInstance().register(progressBarIR) onView(withId(R.id.recyclerView)) .check(matches(withItemCount(13))) IdlingRegistry.getInstance().unregister(progressBarIR) onView(withId(R.id.navigation_profile)) .perform(click()) ...
  7. onView(withId(R.id.email)) login(EMAIL, 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()) } }
  8. onView(withId(R.id.email)) login(EMAIL, PASSWORD) } open class BaseTestRobot { fun login(email:

    String, password: String) { enterText(R.id.email, email) enterText(R.id.password, password) clickOnView(R.id.loginButton) } }
  9. 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
  10. 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
  11. 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) ... }
  12. Accessibility support Accessibility checks Hello … FONT SIZE Small, Default,

    Large, Largest DISPLAY SCALING Small, Default, Large, Largest
  13. databases fi les shared_prefs @get:Rul e val clearDatabaseRule = ClearDatabaseRule(

    ) @get:Rul e val clearFileRule = ClearFilesRule( ) @get:Rul e val clearPreferencesRule = ClearPreferencesRule( ) https://github.com/AdevintaSpain/Barista Clean up data after test execution
  14. Real data vs Fake data 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.
  15. Real data vs Fake data 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
  16. INTERACTION WITH THE SERVER Interaction with the production server. ENTRY

    POINT Similar to the entry point of an app. END-TO-END TESTS
  17. FUNCTIONAL TESTS INTERACTION WITH THE SERVER Interaction with the non-production

    server. NAVIGATION Usually, navigation is not needed. ENTRY POINT We can start the test from a required Activity/Fragment.
  18. 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.
  19. Test case execution - Simulate User actions - Incorrect state

    before/after a test case - Toast, Snackbar, etc
  20. - 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