Fast and stable UI tests in Android

2b0404a5db1a74f01bf3bf94d142e28c?s=47 Alex Zhukovich
June 18, 2020
110

Fast and stable UI tests 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 to your project.
This talk will cover the following topics:
- How to create fast and stable UI tests
- How to avoid flaky tests
- How to share UI tests between local and instrumentation tests
- How DSL can speed up adding stable UI tests to the project

2b0404a5db1a74f01bf3bf94d142e28c?s=128

Alex Zhukovich

June 18, 2020
Tweet

Transcript

  1. None
  2. None
  3. None
  4. None
  5. adb shell am instrument Instrumentation >_

  6. None
  7. None
  8. val textInputEditText = onView( allOf( withId(R.id.emailEditText), childAtPosition( childAtPosition( withId(R.id.emailInputLayout), 0

    ), 0 ) ) ) textInputEditText.perform(scrollTo(), replaceText("test@alexzh.com"), closeSoftKeyboard()) val textInputEditText2 = onView( allOf( withId(R.id.passwordEditText), childAtPosition( childAtPosition( withId(R.id.passwordInputLayout), 0 ), 0 ) ) ) textInputEditText2.perform(scrollTo(), replaceText("test"), closeSoftKeyboard()) val materialButton = onView( allOf( withId(R.id.loginButton), withText("Login"), childAtPosition( childAtPosition( withId(R.id.scrollView2), 0 ), 4 ) ) ) materialButton.perform(scrollTo(), click()) onView(withId(R.id.emailEditText)) .perform(replaceText("test@alexzh.com")) onView(withId(R.id.passwordEditText)) .perform(replaceText("test")) onView(withId(R.id.loginButton)) .perform(click())
  9. None
  10. Approaches and tools

  11. None
  12. Clear data

  13. None
  14. None
  15. End to End Functional

  16. Espresso onView(withId(R.id.email)) .perform(replaceText(EMAIL) onView(withId(R.id.password)) .perform(replaceText(PASSWORD) onView(withId(R.id.login)) .check(matchers(isDisplayed()))

  17. UiAutomator val email = device.findObject(By.res(PACKAGE, "email")) email.text = EMAIL val

    password = device.findObject(By.res(PACKAGE, "password")) email.text = PASSWORD val login = device.findObject(By.res(PACKAGE, "login")) if (login != null) { ... }
  18. Appium val email = driver.findElement(By.id("email")) email.value = EMAIL val password

    = driver.findElement(By.id("password")) password.value = PASSWORD val login = driver.findElement(By.id("login")) login.isDisplayed()
  19. Espresso UiAutomator Appium Support of Android resources

  20. Espresso UiAutomator Appium Support of Android resources Test cases type

  21. Espresso UiAutomator Appium Support of Android resources Test cases type

    Execution time
  22. Clear data

  23. writeTo(R.id.email, EMAIL) writeTo(R.id.password, PASSWORD) clickOn(R.id.login) assertDisplayed(R.id.name, USER_NAME) assertDisplayed(R.id.email, EMAIL) Kakao

  24. writeTo(R.id.email, EMAIL) writeTo(R.id.password, PASSWORD) clickOn(R.id.login) assertDisplayed(R.id.name, USER_NAME) assertDisplayed(R.id.email, EMAIL) @Rule

    ClearPreferencesRule @Rule ClearDatabaseRule @Rule ClearFilesRule Kakao
  25. writeTo(R.id.email, EMAIL) writeTo(R.id.password, PASSWORD) clickOn(R.id.login) assertDisplayed(R.id.name, USER_NAME) assertDisplayed(R.id.email, EMAIL) @Rule

    ClearPreferencesRule @Rule ClearDatabaseRule @Rule ClearFilesRule @Test @AllowFlaky(attempts = 5) @Repeat(times = 5) fun flaky_test() { ... } Kakao
  26. @Test fun should_verify_account_information() { val toolbarVisibilityIR = ViewVisibilityIdlingResource(R.id.toolbar, View.VISIBLE) IdlingRegistry.getInstance().register(toolbarVisibilityIR)

    assertDisplayed(R.id.toolbar) IdlingRegistry.getInstance().unregister(toolbarVisibilityIR) // LOGIN writeTo(R.id.emailEditText, EMAIL) writeTo(R.id.passwordEditText, PASSWORD) closeKeyboard() clickOn(R.id.loginButton) // NAVIGATE TO PROFILE val progressBarIR = ViewVisibilityIdlingResource(R.id.progressBar, View.GONE) IdlingRegistry.getInstance().register(progressBarIR) assertDisplayed(R.id.recyclerView) IdlingRegistry.getInstance().unregister(progressBarIR) clickOn(R.id.navigation_profile) // VERIFY PROFILE INFO val progressProfileIR = ViewVisibilityIdlingResource(R.id.progressBar, View.GONE) IdlingRegistry.getInstance().register(progressProfileIR) assertDisplayed(R.id.nameTextView, USER_NAME) assertDisplayed(R.id.emailTextView, EMAIL) IdlingRegistry.getInstance().unregister(progressProfileIR) // LOGOUT clickMenu(R.id.logout_action) // LOGIN SCREEN intended(hasComponent(LoginActivity::class.java.name)) } Kakao
  27. Kakao onScreen<LoginScreen> { email.replaceText(EMAIL) password { replaceText(PASSWORD) closeSoftKeyboard() } login.click()

    }
  28. onScreen<LoginScreen> { email.replaceText(EMAIL) password { replaceText(PASSWORD) closeSoftKeyboard() } login.click() }

    Kakao
  29. @Test fun should_verify_account_information() { onScreen<LoginScreen> { email.replaceText(EMAIL) password { replaceText(PASSWORD)

    closeSoftKeyboard() } login.click() } onScreen<HomeScreen> { waitCoffeeDrinkList() profileMenuItem.click() } onScreen<ProfileScreen> { waitForProfileInfo() name.hasText(USER_NAME) email.hasText(EMAIL) openMenu() logout.click() } onScreen<LoginScreen> { loginScreenIntent.intended() } } Kakao
  30. onScreen<LoginScreen> { email.replaceText(EMAIL) password { replaceText(PASSWORD) closeSoftKeyboard() } login.click() }

    Kakao
  31. Kakao @Test fun should_verify_account_information() = beforeTest { activityTestRule.launchActivity(intent) }.afterTest {

    ... }.run { onScreen<LoginScreen> { email.replaceText(EMAIL) password { replaceText(PASSWORD) closeSoftKeyboard() } login.click() } onScreen<HomeScreen> { waitCoffeeDrinkList() profileMenuItem.click() } onScreen<ProfileScreen> { waitForProfileInfo() name.hasText(USER_NAME) email.hasText(EMAIL) openMenu() logout.click() } onScreen<LoginScreen> { loginScreenIntent.intended() } }
  32. None
  33. None
  34. @RunWith(AndroidJUnit4::class) class LoginActivityTest { @get:Rule val activityRule = ActivityTestRule( LoginActivity::class.java,

    true, false ) @Test fun superImportantTest() { val intent = ... ativityRule.launchActivity(intent) ... } } @RunWith(AndroidJUnit4::class) class LoginActivityTest { @Test fun superImportantTest() { launchFragmentInContainer<LoginFragment>() ... } }
  35. None
  36. Domain Specific Language

  37. Scenario:

  38. Scenario:

  39. Scenario:

  40. loginScreen { login(EMAIL, PASSWORD) } homeScreen { navigateToProfile() } profileScreen

    { hasEmail(EMAIL) hasUserName(USER_NAME) logout() } loginScreen { isOpen() } loginScreen { enterEmail(EMAIL) enterPassword(PASSWORD) pressLogin() } homeScreen { waitCoffeeDrinks() pressProfile() } profileScreen { waitUserData() hasEmail(EMAIL) hasUserName(USER_NAME) openMenu() pressLogout() } loginScreen { isOpen() }
  41. // LOGIN onView(withId(R.id.email)) .perform(replaceText(EMAIL)) onView(withId(R.id.password)) .perform(replaceText(PASSWORD)) onView(withId(R.id.login)) .perform(click())

  42. // LOGIN onView(withId(R.id.email)) .perform(replaceText(EMAIL)) onView(withId(R.id.password)) .perform(replaceText(PASSWORD)) onView(withId(R.id.login)) .perform(click()) open class

    BaseTestRobot { fun enterText(viewId: Int, text: String) { onView(withId(viewId)) .perform(replaceText(text)) } fun clickOnView(viewId: Int) { onView(withId(viewId)) .perform(click()) } }
  43. // LOGIN onView(withId(R.id.email)) .perform(replaceText(EMAIL)) onView(withId(R.id.password)) .perform(replaceText(PASSWORD)) onView(withId(R.id.login)) .perform(click()) open class

    BaseTestRobot { fun enterText(viewId: Int, text: String) { onView(withId(viewId)) .perform(replaceText(text)) } fun clickOnView(viewId: Int) { onView(withId(viewId)) .perform(click()) } } class LoginScreenRobot : BaseTestRobot() { fun login(email: String, password: String) { enterText(R.id.email, email) enterText(R.id.password, password) clickOnView(R.id.loginButton) } }
  44. // LOGIN onView(withId(R.id.email)) .perform(replaceText(EMAIL)) onView(withId(R.id.password)) .perform(replaceText(PASSWORD)) onView(withId(R.id.login)) .perform(click()) loginScreen {

    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()) } } class LoginScreenRobot : BaseTestRobot() { fun login(email: String, password: String) { enterText(R.id.email, email) enterText(R.id.password, password) clickOnView(R.id.loginButton) } } fun loginScreen(func: LoginScreenRobot.() -> Unit) = LoginScreenRobot().apply { func() }
  45. Test case Screen Robot Basic operations

  46. None
  47. Shared tests

  48. None
  49. android { ... sourceSets { androidTest { java.srcDirs += "src/sharedTest/java"

    } test { java.srcDirs += "src/sharedTest/java" } } } sharedTest
  50. How to start

  51. App flow

  52. Priorities

  53. Analytical data

  54. More