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. 4.
  2. 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())) } }
  3. 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) } } } }
  4. 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()) } }
  5. 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())
  6. 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())
  7. 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())
  8. 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())
  9. 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() } }
  10. 20.

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

    Mocking interaction with server WireMock RestMock MockWebServer
  11. 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
  12. 22.

    User Authorization End To End @Test fun shouldVerifyUserAuthorization() { loginScreen

    { display() } loginScreen { openSignIn() } signInScreen { signIn(email, password) } homeScreen { isMapDisplayed() signOut() } }
  13. 23.

    Authorization of the users DATA UI with mocking @Test fun

    shouldOpenMapScreenAfterSuccessfulSignIn() { prepare(testScope) { mockLocationProvider() mockSuccessfulSignIn(email, password) } signInScreen { signIn(email, password) } homeScreen { isSuccessfullyLoaded() } }
  14. 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
  15. 25.

    Search notes - scenarios Test scenario #1: Display all notes

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