How to create fast and stable UI tests

How to create fast and stable UI tests

Apps are growing. They become more complex and require more testing. It means that it is time to integrate automated tests to your project in an efficient way because they should be fast and stable. In this talk, we will explore how to create fast and stable UI tests, avoid flaky tests, and how DSL can speed up adding stable UI tests.
All examples will be based on Android platforms, but many approaches can be applied to other platforms.

2b0404a5db1a74f01bf3bf94d142e28c?s=128

Alex Zhukovich

September 09, 2020
Tweet

Transcript

  1. How to create fast and stable UI tests @Alex_Zhukovich

  2. None
  3. Non-UI testing UI testing Stress testing View Comparison testing

  4. https://github.com/AlexZhukovich/ImBarista-App

  5. None
  6. None
  7. public class SampleTest { private AndroidDriver driver; @Before public void

    setUp() throws MalformedURLException { DesiredCapabilities desiredCapabilities = new DesiredCapabilities(); desiredCapabilities.setCapability("platformName", "Android"); desiredCapabilities.setCapability("ensureWebviewsHavePages", true); URL remoteUrl = new URL("http://localhost:4723/wd/hub"); driver = new AndroidDriver(remoteUrl, desiredCapabilities); } @Test public void sampleTest() { MobileElement el7 = (MobileElement) driver.findElementById("com.alexzh.imbarista:id/emailEditText"); el7.sendKeys("test@alexzh.com"); MobileElement el8 = (MobileElement) driver.findElementById("com.alexzh.imbarista:id/passwordEditText"); el8.sendKeys("test"); MobileElement el9 = (MobileElement) driver.findElementById("com.alexzh.imbarista:id/loginButton"); el9.click(); MobileElement el10 = (MobileElement) driver.findElementByAccessibilityId("Profile"); el10.click(); MobileElement el11 = (MobileElement) driver.findElementByAccessibilityId("More options"); el11.click(); MobileElement el12 = (MobileElement) driver.findElementByXPath("/hierarchy/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.ListView/android.widget.LinearLayout[2] /android.widget.LinearLayout/android.widget.RelativeLayout/android.widget.TextView"); el12.click(); } @After public void tearDown() { driver.quit(); } } Appium Recorder
  8. @RunWith(AndroidJUnit4.class) public class RecorderActivityTest { @Rule public ActivityTestRule<SplashActivity> mActivityTestRule =

    new ActivityTestRule<>(SplashActivity.class); @Test public void recorderActivityTest() { ViewInteraction textInputEditText = onView( allOf(withId(R.id.emailEditText), childAtPosition(childAtPosition(withId(R.id.emailInputLayout),0),0))); textInputEditText.perform(scrollTo(), replaceText("test@alexzh.com"), closeSoftKeyboard()); ViewInteraction textInputEditText2 = onView( allOf(withId(R.id.passwordEditText),childAtPosition(childAtPosition(withId(R.id.passwordInputLayout), 0), 0))); textInputEditText2.perform(scrollTo(), replaceText("test"), closeSoftKeyboard()); ViewInteraction materialButton = onView( allOf(withId(R.id.loginButton), withText("Login"), childAtPosition(childAtPosition(withId(R.id.scrollView2), 0), 4))); materialButton.perform(scrollTo(), click()); ViewInteraction bottomNavigationItemView = onView( allOf( withId(R.id.navigation_profile), withContentDescription("Profile"), childAtPosition(childAtPosition(withId(R.id.navigation), 0), 2), isDisplayed() )); bottomNavigationItemView.perform(click()); ViewInteraction overflowMenuButton = onView( allOf(withContentDescription("More options"), childAtPosition(childAtPosition(withId(R.id.toolbar), 1), 0), isDisplayed())); overflowMenuButton.perform(click()); ViewInteraction appCompatTextView = onView( allOf(withId(R.id.title), withText("Log Out"), childAtPosition(childAtPosition(withId(R.id.content), 0), 0), isDisplayed())); appCompatTextView.perform(click()); } ... } Espresso Test Recorder
  9. 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())
  10. Changes in app Network speed Unreadability Test data

  11. None
  12. WWW Android iOS

  13. developer.android.com

  14. @Test fun shouldDisplayErrorWhenPasswordIsBlank() { val email = "test-account@alexzh.com" val password

    = "" onView(withId(R.id.emailEditText)) .perform(replaceText(email)) onView(withId(R.id.passwordEditText)) .perform(replaceText(password)) onView(withId(R.id.loginButton)) .perform(click()) onView(withText(R.string.password_is_blank)) .check(matches(isDisplayed())) }
  15. 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()) ...
  16. // -- LOGIN -- 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()) ...
  17. Domain Specific Language

  18. Scenario: 1. Login to application

  19. Scenario: 1. Login to application 2. Open Profile screen 3.

    Check name and email
  20. Scenario: 1. Login to application 2. Open Profile screen 3.

    Check name and email 4. Open menu 5. Logout from application
  21. loginScreen { enterEmail(EMAIL) enterPassword(PASSWORD) pressLogin() } homeScreen { waitCoffeeDrinks() pressProfile()

    } profileScreen { waitUserData() hasEmail(EMAIL) hasUserName(USER_NAME) openMenu() pressLogout() } loginScreen { isOpen() } loginScreen { login(EMAIL, PASSWORD) } homeScreen { navigateToProfile() } profileScreen { hasEmail(EMAIL) hasUserName(USER_NAME) logout() } loginScreen { isOpen() }
  22. // LOGIN onView(withId(R.id.email)) .perform(replaceText(EMAIL)) onView(withId(R.id.password)) .perform(replaceText(PASSWORD)) onView(withId(R.id.login)) .perform(click())

  23. // 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()) } }
  24. // 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) } }
  25. // 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) } } fun loginScreen( func: LoginScreenRobot.() -> Unit ) = LoginScreenRobot().apply { func() }
  26. // 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() }
  27. loginScreen { enterEmail(EMAIL) enterPassword(PASSWORD) pressLogin() } homeScreen { waitCoffeeDrinks() pressProfile()

    } profileScreen { waitUserData() hasEmail(EMAIL) hasUserName(USER_NAME) openMenu() pressLogout() } loginScreen { isOpen() } loginScreen { login(EMAIL, PASSWORD) } homeScreen { navigateToProfile() } profileScreen { hasEmail(EMAIL) hasUserName(USER_NAME) logout() } loginScreen { isOpen() }
  28. Test case Screen Robot Basic operations

  29. Readability Reusability Building complexity Framework independant

  30. Approaches and tools

  31. None
  32. Clear data

  33. None
  34. Mock Fake

  35. End to End tests Functional tests 1 1 Entry point

    Start test from the main screen 2 Interaction with server Verification interaction with server 3 Navigation Verification navigation between screens Entry point Start application from any screen 2 Interaction with server Usually no interaction with prod server 3 Navigation Usually no navigation between screen Navigation between neighbor screens 4 UI verification Fast verification of UI components 4 App verification Slow verification of entire product
  36. End To End tests Functional tests

  37. How to start

  38. App flow Priorities Analytical data

  39. App flow Priorities Analytical data

  40. App flow Priorities Analytical data

  41. Flaky tests

  42. Testing approaches Tools Parallel execution Test data

  43. FAILED TESTS FLAKY TESTS

  44. Android Test Orchestrator @Rule ClearPreferencesRule ClearDatabaseRule ClearFilesRule Interceptors @AllowFlaky @

  45. Good practices

  46. Use meaningful names for test cases

  47. All test cases should be independent

  48. Never use sleep in test code

  49. Do not entirely rely on test automation

  50. #ExploreMore UI testing alexzh.com @AlexZhukovich