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 full-size slide

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

    View full-size slide

  3. Layout Inspector
    UiAutomator Viewer

    View full-size slide

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

    View full-size slide

  5. Application overview

    View full-size 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 full-size slide

  7. Espresso Test Recorder

    View full-size slide

  8. Espresso Test Recorder

    View full-size 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 full-size 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 full-size slide

  11. Domain-specific
    language

    View full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size slide

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

    View full-size slide

  19. Testing
    interaction
    with a server

    View full-size slide

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

    View full-size 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 full-size slide

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

    View full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size slide

  28. Handle error during loading notes
    !

    View full-size 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 full-size slide

  30. Should we use UI test with mocking
    everywhere?

    View full-size slide

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

    View full-size slide

  32. Scope of Regression testing

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  35. Production and testing tools
    Tools Emulator

    View full-size slide

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

    View full-size slide

  37. Handling Flaky tests on CI

    View full-size slide

  38. Handling Flaky tests on CI

    View full-size slide

  39. Handling Flaky tests on CI
    FLAKY TESTS

    View full-size slide

  40. Handling Flaky tests on CI
    FLAKY TESTS FAILED TESTS

    View full-size slide

  41. Testing tips

    View full-size slide

  42. The name matters

    View full-size slide

  43. Learn existing test cases
    and maintain them

    View full-size slide

  44. Avoid redundant test data

    View full-size slide

  45. Verify positive and negative
    test cases

    View full-size slide

  46. Verify business and
    navigation flows

    View full-size slide

  47. Test only your code

    View full-size slide

  48. Stop testing everything with
    one type of tests

    View full-size slide

  49. Stop collecting flaky tests

    View full-size slide

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

    View full-size slide

  51. Covering production bugs in
    test cases

    View full-size slide

  52. Stop testing manually, just
    automate it

    View full-size slide

  53. Care about testability in the
    code

    View full-size slide

  54. 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 full-size slide