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

2b0404a5db1a74f01bf3bf94d142e28c?s=128

Alex Zhukovich

October 20, 2018
Tweet

Transcript

  1. Efficient UI testing Android apps by example @Alex_Zhukovich

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

    Instrumentation API
  3. Layout Inspector UiAutomator Viewer

  4. Installing and running test cases on device adb shell Instrumentation

    am instrument Application Test Application
  5. Application overview

  6. @RunWith(AndroidJUnit4::class) class SignInActivityTest { private val correctEmail = "test@test.com" @Rule

    @JvmField val activityRule = ActivityTestRule<SignInActivity>(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())) } }
  7. Espresso Test Recorder

  8. Espresso Test Recorder

  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("test@test.com"), 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<View>, position: Int): Matcher<View> { return object : TypeSafeMatcher<View>() { 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) } } } }
  10. @RunWith(AndroidJUnit4::class) class SmokeTests { private val correctEmail = "test@test.com" private

    val correctPassword = "test123" @Rule @JvmField val chain: RuleChain = RuleChain .outerRule(GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION)) .around(ActivityTestRule<SplashActivity>(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()) } }
  11. Domain-specific language

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

  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())
  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())
  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())
  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())
  17. @RunWith(AndroidJUnit4::class) class SmokeTests { private val correctEmail = "test@test.com" private

    val correctPassword = "test123" @Rule @JvmField val chain: RuleChain = RuleChain .outerRule(GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION)) .around(ActivityTestRule<SplashActivity>(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() } }
  18. Internal DSL(Domain-specific language) Readability Reuse Error recovery Building complexity Flexibility

  19. Testing interaction with a server

  20. Testing interaction with a server Mock layer Interaction with server(s)

    Mocking interaction with server WireMock RestMock MockWebServer
  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
  22. User Authorization End To End @Test fun shouldVerifyUserAuthorization() { loginScreen

    { display() } loginScreen { openSignIn() } signInScreen { signIn(email, password) } homeScreen { isMapDisplayed() signOut() } }
  23. Authorization of the users DATA UI with mocking @Test fun

    shouldOpenMapScreenAfterSuccessfulSignIn() { prepare(testScope) { mockLocationProvider() mockSuccessfulSignIn(email, password) } signInScreen { signIn(email, password) } homeScreen { isSuccessfullyLoaded() } }
  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
  25. Search notes - scenarios Test scenario #1: Display all notes

    Test scenario #2: Handle error during loading notes Test scenario #3: Display search results
  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() } }
  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) } }
  28. Handle error during loading notes !

  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
  30. Should we use UI test with mocking everywhere?

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

  32. Scope of Regression testing

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

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

  35. Flaky tests

  36. Production and testing tools Tools Emulator

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

    running test Parallel execution
  38. Handling Flaky tests on CI

  39. Handling Flaky tests on CI

  40. Handling Flaky tests on CI FLAKY TESTS

  41. Handling Flaky tests on CI FLAKY TESTS FAILED TESTS

  42. Testing tips

  43. The name matters

  44. Learn existing test cases and maintain them

  45. Avoid redundant test data

  46. Verify positive and negative test cases

  47. Verify business and navigation flows

  48. Test only your code

  49. Stop testing everything with one type of tests

  50. Stop collecting flaky tests

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

  52. Covering production bugs in test cases

  53. Stop testing manually, just automate it

  54. Care about testability in the code

  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