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

Efficient UI testing Android apps by example

Efficient UI testing Android apps by example

This talk will cover:
- analyzing general use cases which apply to many applications
- tools which can help us with efficient testing
- efficient combination of different types of UI tests
- best practices for adding and maintaining test cases to your project

Alex Zhukovich

October 20, 2018
Tweet

More Decks by Alex Zhukovich

Other Decks in Technology

Transcript

  1. Efficient UI testing
    Android apps
    by example
    @Alex_Zhukovich

    View Slide

  2. Android tests
    Local Instrumentation
    UI Non-UI
    Robolectric
    Espresso
    UiAutomator
    Appium
    Instrumentation API

    View Slide

  3. Layout Inspector
    UiAutomator Viewer

    View Slide

  4. Installing and running test cases on device
    adb shell
    Instrumentation
    am instrument
    Application
    Test Application

    View Slide

  5. Application overview

    View Slide

  6. @RunWith(AndroidJUnit4::class)
    class SignInActivityTest {
    private val correctEmail = "[email protected]"
    @Rule @JvmField
    val activityRule = ActivityTestRule(SignInActivity::class.java)
    @Test
    fun shouldDisplayPasswordErrorWhenPasswordIsEmpty() {
    onView(withId(R.id.email))
    .perform(replaceText(correctEmail))
    onView(withId(R.id.signIn))
    .perform(click())
    onView(withText(R.string.error_password_should_not_be_empty))
    .check(matches(isDisplayed()))
    }
    }

    View Slide

  7. Espresso Test Recorder

    View Slide

  8. Espresso Test Recorder

    View Slide

  9. Espresso Test Recorder @LargeTest
    @RunWith(AndroidJUnit4::class)
    class SignInActivityTest {
    @Rule
    @JvmField
    var mActivityTestRule = ActivityTestRule(SignInActivity::class.java)
    @Test
    fun signInActivityTest() {
    Thread.sleep(7000)
    val appCompatEditText = onView(
    allOf(withId(R.id.email),
    childAtPosition(
    allOf(withId(R.id.signInRoot),
    childAtPosition(
    withId(android.R.id.content),
    0)),
    4),
    isDisplayed()))
    appCompatEditText.perform(click())
    val appCompatEditText2 = onView(
    allOf(withId(R.id.email),
    childAtPosition(
    allOf(withId(R.id.signInRoot),
    childAtPosition(
    withId(android.R.id.content),
    0)),
    4),
    isDisplayed()))
    appCompatEditText2.perform(replaceText("[email protected]"), closeSoftKeyboard())
    Thread.sleep(7000)
    pressBack()
    val appCompatButton = onView(
    allOf(withId(R.id.signIn), withText("Sing In"),
    childAtPosition(
    allOf(withId(R.id.signInRoot),
    childAtPosition(
    withId(android.R.id.content),
    0)),
    6),
    isDisplayed()))
    appCompatButton.perform(click())
    }
    private fun childAtPosition(
    parentMatcher: Matcher, position: Int): Matcher {
    return object : TypeSafeMatcher() {
    override fun describeTo(description: Description) {
    description.appendText("Child at position $position in parent ")
    parentMatcher.describeTo(description)
    }
    public override fun matchesSafely(view: View): Boolean {
    val parent = view.parent
    return parent is ViewGroup && parentMatcher.matches(parent)
    && view == parent.getChildAt(position)
    }
    }
    }
    }

    View Slide

  10. @RunWith(AndroidJUnit4::class)
    class SmokeTests {
    private val correctEmail = "[email protected]"
    private val correctPassword = "test123"
    @Rule
    @JvmField
    val chain: RuleChain = RuleChain
    .outerRule(GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION))
    .around(ActivityTestRule(SplashActivity::class.java, true, false))
    @Test
    fun shouldVerifySuccessfulLogin() {
    val mapVisibilityIdlingResource =
    ViewVisibilityIdlingResource(R.id.mapContainer, View.VISIBLE)
    splashActivityE2ETestRule.launchActivity(null)
    onView(withId(R.id.signIn))
    .perform(click())
    onView(withId(R.id.email))
    .perform(replaceText(correctEmail), closeSoftKeyboard())
    onView(withId(R.id.password))
    .perform(replaceText(correctPassword), closeSoftKeyboard())
    onView(withId(R.id.signIn))
    .perform(click())
    IdlingRegistry.getInstance().register(mapVisibilityIdlingResource)
    onView(withId(R.id.mapContainer))
    .check(ViewAssertions.matches(isDisplayed()))
    IdlingRegistry.getInstance().unregister(mapVisibilityIdlingResource)
    openActionBarOverflowOrOptionsMenu(getActivityInstance())
    onView(withText(R.string.nav_sign_out_title))
    .check(ViewAssertions.matches(isDisplayed()))
    .perform(click())
    }
    }

    View Slide

  11. Domain-specific
    language

    View Slide

  12. onView(withId(R.id.email))
    .perform(replaceText(email), closeSoftKeyboard())
    onView(withId(R.id.password))
    .perform(replaceText(password), closeSoftKeyboard())
    onView(withId(R.id.signIn))
    .perform(click())

    View Slide

  13. open class BaseTestRobot {
    fun enterText(viewId: Int, text: String): ViewInteraction =
    onView(withId(viewId))
    .perform(replaceText(text), closeSoftKeyboard())
    fun clickView(viewId: Int): ViewInteraction =
    onView(withId(viewId))
    .perform(click())
    }
    onView(withId(R.id.email))
    .perform(replaceText(email), closeSoftKeyboard())
    onView(withId(R.id.password))
    .perform(replaceText(password), closeSoftKeyboard())
    onView(withId(R.id.signIn))
    .perform(click())

    View Slide

  14. class SignInScreenRobot : BaseTestRobot() {
    fun signIn(email: String, password: String) {
    enterText(R.id.email, email)
    enterText(R.id.password, password)
    clickView(R.id.signIn)
    }
    }
    open class BaseTestRobot {
    fun enterText(viewId: Int, text: String): ViewInteraction =
    onView(withId(viewId))
    .perform(replaceText(text), closeSoftKeyboard())
    fun clickView(viewId: Int): ViewInteraction =
    onView(withId(viewId))
    .perform(click())
    }
    onView(withId(R.id.email))
    .perform(replaceText(email), closeSoftKeyboard())
    onView(withId(R.id.password))
    .perform(replaceText(password), closeSoftKeyboard())
    onView(withId(R.id.signIn))
    .perform(click())

    View Slide

  15. class SignInScreenRobot : BaseTestRobot() {
    fun signIn(email: String, password: String) {
    enterText(R.id.email, email)
    enterText(R.id.password, password)
    clickView(R.id.signIn)
    }
    }
    open class BaseTestRobot {
    fun enterText(viewId: Int, text: String): ViewInteraction =
    onView(withId(viewId))
    .perform(replaceText(text), closeSoftKeyboard())
    fun clickView(viewId: Int): ViewInteraction =
    onView(withId(viewId))
    .perform(click())
    }
    fun signInScreen(func: SignInScreenRobot.() -> Unit) =
    SignInScreenRobot().apply { func() }
    onView(withId(R.id.email))
    .perform(replaceText(email), closeSoftKeyboard())
    onView(withId(R.id.password))
    .perform(replaceText(password), closeSoftKeyboard())
    onView(withId(R.id.signIn))
    .perform(click())

    View Slide

  16. class SignInScreenRobot : BaseTestRobot() {
    fun signIn(email: String, password: String) {
    enterText(R.id.email, email)
    enterText(R.id.password, password)
    clickView(R.id.signIn)
    }
    }
    open class BaseTestRobot {
    fun enterText(viewId: Int, text: String): ViewInteraction =
    onView(withId(viewId))
    .perform(replaceText(text), closeSoftKeyboard())
    fun clickView(viewId: Int): ViewInteraction =
    onView(withId(viewId))
    .perform(click())
    }
    fun signInScreen(func: SignInScreenRobot.() -> Unit) =
    SignInScreenRobot().apply { func() }
    signInScreen {
    signIn(email, password)
    }
    onView(withId(R.id.email))
    .perform(replaceText(email), closeSoftKeyboard())
    onView(withId(R.id.password))
    .perform(replaceText(password), closeSoftKeyboard())
    onView(withId(R.id.signIn))
    .perform(click())

    View Slide

  17. @RunWith(AndroidJUnit4::class)
    class SmokeTests {
    private val correctEmail = "[email protected]"
    private val correctPassword = "test123"
    @Rule
    @JvmField
    val chain: RuleChain = RuleChain
    .outerRule(GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION))
    .around(ActivityTestRule(SplashActivity::class.java, true, false))
    @Test
    fun shouldVerifySuccessfulLogin() {
    val mapVisibilityIdlingResource =
    ViewVisibilityIdlingResource(R.id.mapContainer, View.VISIBLE)
    splashActivityE2ETestRule.launchActivity(null)
    onView(withId(R.id.signIn))
    .perform(click())
    onView(withId(R.id.email))
    .perform(replaceText(correctEmail), closeSoftKeyboard())
    onView(withId(R.id.password))
    .perform(replaceText(correctPassword), closeSoftKeyboard())
    onView(withId(R.id.signIn))
    .perform(click())
    IdlingRegistry.getInstance().register(mapVisibilityIdlingResource)
    onView(withId(R.id.mapContainer))
    .check(ViewAssertions.matches(isDisplayed()))
    IdlingRegistry.getInstance().unregister(mapVisibilityIdlingResource)
    openActionBarOverflowOrOptionsMenu(getActivityInstance())
    onView(withText(R.string.nav_sign_out_title))
    .check(ViewAssertions.matches(isDisplayed()))
    .perform(click())
    }
    }
    @Test
    fun shouldVerifySuccessfulLogin() {
    splashScreen {
    display()
    }
    loginScreen {
    openSignIn()
    }
    signInScreen {
    signIn(email, password)
    }
    homeScreen {
    isMapDisplayed()
    signOut()
    }
    }

    View Slide

  18. Internal DSL(Domain-specific language)
    Readability Reuse Error recovery
    Building complexity Flexibility

    View Slide

  19. Testing
    interaction
    with a server

    View Slide

  20. Testing interaction with a server
    Mock layer
    Interaction with
    server(s)
    Mocking interaction
    with server
    WireMock
    RestMock
    MockWebServer

    View Slide

  21. Authorization of the user - scenarios
    Test scenario #1:
    Enter correct auth data
    Test scenario #2:
    Enter incorrect auth data
    Test scenario #3:
    Enter incorrect auth data and handle them on client side

    View Slide

  22. User Authorization End To End
    @Test
    fun shouldVerifyUserAuthorization() {
    loginScreen {
    display()
    }
    loginScreen {
    openSignIn()
    }
    signInScreen {
    signIn(email, password)
    }
    homeScreen {
    isMapDisplayed()
    signOut()
    }
    }

    View Slide

  23. Authorization of the users
    DATA
    UI with mocking
    @Test
    fun shouldOpenMapScreenAfterSuccessfulSignIn() {
    prepare(testScope) {
    mockLocationProvider()
    mockSuccessfulSignIn(email, password)
    }
    signInScreen {
    signIn(email, password)
    }
    homeScreen {
    isSuccessfullyLoaded()
    }
    }

    View Slide

  24. Authorization of the user – differences
    End-To-End test cases UI tests with mocking
    B
    Interaction with server
    Verification interaction with a server
    C Fast UI verification
    Fast and independent on resources tests
    A
    Entry point
    Start tests from the main screen
    B No interaction with server
    Verification UI and interaction with mock object
    A Entry point
    Start test from any screen of the app

    View Slide

  25. Search notes - scenarios
    Test scenario #1:
    Display all notes
    Test scenario #2:
    Handle error during loading notes
    Test scenario #3:
    Display search results

    View Slide

  26. Display search results End To End
    @Test
    fun shouldVerifyAddingAndSearchNote() {
    val noteText = "test note ${Date().time}"
    splashScreen {
    display()
    }
    loginScreen {
    openSignIn()
    }
    signInScreen {
    signIn(email, password)
    }
    homeScreen {
    isMapDisplayed()
    openAddNoteFragment()
    addNote(noteText)
    openSearchFragment()
    searchNoteByText(noteText)
    isNoteInSearchResult(noteText)
    signOut()
    }
    }

    View Slide

  27. Display search results
    DATA
    UI with mocking
    @Test
    fun shouldSearchByNotesAndDisplayResult() {
    val expectedItemCount = testNotes.size
    prepare(testScope) {
    mockLoadingEmptyListOfNotes()
    mockSearchNoteByAnyText(testNotes)
    }
    homeScreen {
    searchNoteByText(searchInput)
    verifySearchResultsByItemCount(expectedItemCount)
    verifySearchResults(testNotes)
    }
    }

    View Slide

  28. Handle error during loading notes
    !

    View Slide

  29. Search notes – differences
    End-To-End test cases UI tests cases with mocking
    B
    Interaction with server
    Verification interaction with a server
    A
    Entry point
    Start tests from the main screen
    C
    Data from the server
    Depend on data from the server
    E Fast UI verification
    Fast and independent on resources tests
    B No interaction with server
    Verification UI and interaction with mock object
    A Entry point
    Start test from any screen of the app
    D App architecture
    Architecture should support mocking
    C UI component verification
    Testing only fragments, view without main Activity

    View Slide

  30. Should we use UI test with mocking
    everywhere?

    View Slide

  31. Should we use UI tests with mocking everywhere?
    https://gph.is/1bkaInz

    View Slide

  32. Scope of Regression testing

    View Slide

  33. E2E test cases in scope of regression tests
    Specifications
    Analytical data

    View Slide

  34. Efficient UI testing
    End To End tests UI tests
    +

    View Slide

  35. Flaky tests

    View Slide

  36. Production and testing tools
    Tools Emulator

    View Slide

  37. Test cases and test execution process
    Dependencies
    Test data
    Long running test
    Parallel execution

    View Slide

  38. Handling Flaky tests on CI

    View Slide

  39. Handling Flaky tests on CI

    View Slide

  40. Handling Flaky tests on CI
    FLAKY TESTS

    View Slide

  41. Handling Flaky tests on CI
    FLAKY TESTS FAILED TESTS

    View Slide

  42. Testing tips

    View Slide

  43. The name matters

    View Slide

  44. Learn existing test cases
    and maintain them

    View Slide

  45. Avoid redundant test data

    View Slide

  46. Verify positive and negative
    test cases

    View Slide

  47. Verify business and
    navigation flows

    View Slide

  48. Test only your code

    View Slide

  49. Stop testing everything with
    one type of tests

    View Slide

  50. Stop collecting flaky tests

    View Slide

  51. Write test case base on
    specification, not on
    implementation

    View Slide

  52. Covering production bugs in
    test cases

    View Slide

  53. Stop testing manually, just
    automate it

    View Slide

  54. Care about testability in the
    code

    View Slide

  55. Q&A
    Espresso:
    https://developer.android.com/training/testing/espresso/
    UiAutomator:
    https://developer.android.com/training/testing/ui-automator/
    Appium:
    http://appium.io/
    Android Testing codelab:
    https://codelabs.developers.google.com/codelabs/android-testing/
    Instrumentation Testing Robots
    https://academy.realm.io/posts/kau-jake-wharton-testing-robots/
    MapNotes:
    https://github.com/AlexZhukovich/MapNotes
    Blog:
    http://alexzh.com/
    @Alex_Zhukovich

    View Slide