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

How to create fast and stable UI tests

Alex Zhukovich
September 09, 2020

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.

Alex Zhukovich

September 09, 2020
Tweet

More Decks by Alex Zhukovich

Other Decks in Technology

Transcript

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

    View Slide

  2. View Slide

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

    View Slide

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

    View Slide

  5. View Slide

  6. View Slide

  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("[email protected]");
    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

    View Slide

  8. @RunWith(AndroidJUnit4.class)
    public class RecorderActivityTest {
    @Rule
    public ActivityTestRule 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("[email protected]"), 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

    View Slide

  9. val textInputEditText = onView(
    allOf(
    withId(R.id.emailEditText),
    childAtPosition(
    childAtPosition(
    withId(R.id.emailInputLayout),
    0
    ),
    0
    )
    )
    )
    textInputEditText.perform(scrollTo(), replaceText("[email protected]"),
    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("[email protected]"))
    onView(withId(R.id.passwordEditText))
    .perform(replaceText("test"))
    onView(withId(R.id.loginButton))
    .perform(click())

    View Slide

  10. Changes in app Network speed Unreadability Test data

    View Slide

  11. View Slide

  12. WWW
    Android iOS

    View Slide

  13. developer.android.com

    View Slide

  14. @Test
    fun shouldDisplayErrorWhenPasswordIsBlank() {
    val email = "[email protected]"
    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()))
    }

    View Slide

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

    View Slide

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

    View Slide

  17. Domain Specific
    Language

    View Slide

  18. Scenario:
    1. Login to application

    View Slide

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

    View Slide

  20. Scenario:
    1. Login to application
    2. Open Profile screen
    3. Check name and email
    4. Open menu
    5. Logout from application

    View Slide

  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()
    }

    View Slide

  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())

    View Slide

  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())
    }
    }

    View Slide

  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)
    }
    }

    View Slide

  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() }

    View Slide

  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() }

    View Slide

  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()
    }

    View Slide

  28. Test case
    Screen Robot
    Basic operations

    View Slide

  29. Readability Reusability Building
    complexity
    Framework
    independant

    View Slide

  30. Approaches and tools

    View Slide

  31. View Slide

  32. Clear data

    View Slide

  33. View Slide

  34. Mock
    Fake

    View Slide

  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

    View Slide

  36. End To End tests Functional tests

    View Slide

  37. How to start

    View Slide

  38. App flow
    Priorities
    Analytical data

    View Slide

  39. App flow
    Priorities
    Analytical data

    View Slide

  40. App flow
    Priorities
    Analytical data

    View Slide

  41. Flaky tests

    View Slide

  42. Testing
    approaches
    Tools Parallel execution Test data

    View Slide

  43. FAILED TESTS
    FLAKY TESTS

    View Slide

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

    View Slide

  45. Good practices

    View Slide

  46. Use meaningful names
    for test cases

    View Slide

  47. All test cases should be
    independent

    View Slide

  48. Never use sleep in test
    code

    View Slide

  49. Do not entirely rely on
    test automation

    View Slide

  50. #ExploreMore
    UI testing
    alexzh.com @AlexZhukovich

    View Slide