UI Testing with Espresso and Kakao

User interface (UI) testing lets you ensure that your app meets its functional requirements and achieves a high standard of quality such that it is more likely to be successfully adopted by users.

One approach to UI testing is to simply have a human tester perform a set of user operations on the target app and verify that it is behaving correctly. However, this manual approach can be time-consuming, tedious, and error-prone. A more efficient approach is to write your UI tests such that user actions are performed in an automated way. The automated approach allows you to run your tests quickly and reliably in a repeatable manner.


Espresso

The Espresso testing framework, provided by AndroidX Test, provides APIs for writing UI tests to simulate user interactions within a single target app. Espresso tests can run on devices running Android 4.0.1.

A key benefit of using Espresso is that it provides automatic synchronization of test actions with the UI of the app you are testing. Espresso detects when the main thread is idle, so it is able to run your test commands at the appropriate time, improving the reliability of your tests.

The core API is small, predictable, and easy to learn and yet remains open for customization. Espresso tests state expectations, interactions, and assertions clearly without the distraction of boilerplate content, custom infrastructure, or messy implementation details getting in the way.

Espresso tests run optimally fast! It lets you leave your waits, syncs, sleeps, and polls behind while it manipulates and asserts on the application UI when it is at rest.

Implementations

To add Espresso dependencies to your project, complete the following steps:

Open your app’s build.gradle file. This is usually not the top-level build.gradle file but app/build.gradle.

Add the following lines inside dependencies:

androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0'
androidTestImplementation 'androidx.test:runner:1.1.0'
androidTestImplementation 'androidx.test:rules:1.1.0'

To avoid flakiness, we highly recommend that you turn off system animations on the virtual or physical devices used for testing. On your device, under Settings > Developer options, disable the following 3 settings:

  • Window animation scale
  • Transition animation scale
  • Animator duration scale

Basics

The main components of the Espresso include the following:

• Espresso — Entry point to interactions with views (via onView() and onData()). Also exposes APIs that are not necessarily tied to any view, such as pressBack().

  • ViewMatchers — A collection of objects that implement the Matcher<? super View> interface. You can pass one or more of these to the onView() method to locate a view within the current view hierarchy.
onView(allOf(withId(R.id.my_view), withText("Hello!")))
onView(allOf(withId(R.id.my_view), not(withText("Unwanted"))))

If you are not able to narrow down a search using withText() or withContentDescription(), consider treating it as an accessibility bug.

If a view is uniquely identifiable by its text, you need not specify that the view is also assignable from TextView. For a lot of views the R.id of the view should be sufficient.

If the target view is inside an AdapterView — such as ListView, GridView, or Spinner — the onView() method might not work. In these cases, you should use onData() instead.

  • ViewActions — A collection of ViewAction objects that can be passed to the ViewInteraction.perform() method, such as click().
onView(...).perform(click())
onView(...).perform(typeText("Hello"), click())
onView(...).perform(scrollTo(), click())

The scrollTo() method will have no effect if the view is already displayed. Tests run on both smaller and larger screen resolutions.

  • ViewAssertions — A collection of ViewAssertion objects that can be passed the ViewInteraction.check() method. Most of the time, you will use the matches assertion, which uses a View matcher to assert the state of the currently selected view.
onView(…).check(matches(withText("Hello!")))

Rules of Espresso

  • If your testing consists of a single view element then onView(…) is recommended for testing.
onView(ViewMatcher)
	.perform(ViewActions)
	.check(ViewAssertion)

If your testing consists AdapterView type of test then using onData(…) is highly recommended to evert any unwanted errors in your testing.

onData(ObjectMatcher)
	.DataOptions
	.perform(ViewActions)
	.check(ViewAssertions)

If your testing consists of intents and if your waiting return value that comes from that intent such as a sign-in option then using .respondWith(…) is recommended.

intending(IntentMatcher)
	.respondWith(ActivityResults)

If your testing consists of just passing data between two activities you may not have to use respond with to extend your object.

intended(IntentMatchers)

Test Cases

Spinner Test Case

Open the item selection

onView(withId(R.id.spinner_simple)).perform(click())

Select an item

By using onData() we force our desired element into the view hierarchy. The items in the Spinner are strings, so we want to match an item that is equal to the String “Americano”:


onData(allOf(`is`(instanceOf(String::class.java)),
        `is`("Americano"))).perform(click()) 

Verify text is correct

onView(withId(R.id.spinnertext_simple))
    .check(matches(withText(containsString("Americano"))))

Recycler Test Case

RecyclerView objects work differently than AdapterView objects, so onData() cannot be used to interact with them.

To interact with RecyclerViews using Espresso, you can use the espresso-contrib package, which has a collection of RecyclerViewActions that can be used to scroll to positions or to perform actions on items:

  • scrollTo() — Scrolls to the matched View, if it exists.
  • scrollToHolder() — Scrolls to the matched View Holder, if it exists.
  • scrollToPosition() — Scrolls to a specific position.
  • actionOnHolderItem() — Performs a View Action on a matched View Holder.
  • actionOnItem() — Performs a View Action on a matched View.
  • actionOnItemAtPosition() — Performs a ViewAction on a view at a specific position.

The following snippets feature some examples from the RecyclerViewSample sample:

@Test(expected = PerformException::class)
fun itemWithText_doesNotExist() {
    // Attempt to scroll to an item that contains the special text.
    onView(ViewMatchers.withId(R.id.recyclerView))
        .perform(
            // scrollTo will fail the test if no item matches.
            RecyclerViewActions.scrollTo(
                hasDescendant(withText("not in the list"))
            )
        )
}
@Test fun scrollToItemBelowFold_checkItsText() {
    // First, scroll to the position that needs to be matched and click on it.
    onView(ViewMatchers.withId(R.id.recyclerView))
        .perform(
            RecyclerViewActions.actionOnItemAtPosition(
                ITEM_BELOW_THE_FOLD,
                click()
            )
        )

    // Match the text in an item below the fold and check that it's displayed.
    val itemElementText = "${activityRule.activity.resources
        .getString(R.string.item_element_text)} ${ITEM_BELOW_THE_FOLD.toString()}"
    onView(withText(itemElementText)).check(matches(isDisplayed()))
}
@Test fun itemInMiddleOfList_hasSpecialText() {
    // First, scroll to the view holder using the isInTheMiddle() matcher.
    onView(ViewMatchers.withId(R.id.recyclerView))
        .perform(RecyclerViewActions.scrollToHolder(isInTheMiddle()))

    // Check that the item has the special text.
    val middleElementText = activityRule.activity.resources
            .getString(R.string.middle)
    onView(withText(middleElementText)).check(matches(isDisplayed()))
}

Intent Test Case

Espresso-Intents is an extension to Espresso, which enables validation and stubbing of intents sent out by the application under test. It’s like Mockito, but for Android Intents.

If your app delegates functionality to other apps or the platform, you can use Espresso-Intents to focus on your own app’s logic while assuming that other apps or the platform will function correctly. With Espresso-Intents, you can match and validate your outgoing intents or even provide stub responses in place of actual intent responses.

Let’s start by adding implementation:

androidTestImplementation 'androidx.test.espresso:espresso-intents:3.1.0'

Before writing an Espresso-Intents test, set up an IntentsTestRule. This is an extension of the class ActivityTestRule and makes it easy to use Espresso-Intents APIs in functional UI tests. An IntentsTestRule initializes Espresso-Intents before each test annotated with @Test and releases Espresso-Intents after each test run.

@get:Rule
val intent = IntentsTestRule(MyActivity::class.java) 

Snippet shows intent validation that uses existing intent matchers that matches an outgoing intent that starts a browser:

assertThat(intent).hasAction(Intent.ACTION_VIEW)
assertThat(intent).categories().containsExactly(Intent.CATEGORY_BROWSABLE)
assertThat(intent).hasData(Uri.parse("www.google.com"))
assertThat(intent).extras().containsKey("key1")
assertThat(intent).extras().string("key1").isEqualTo("value1")
assertThat(intent).extras().containsKey("key2")
assertThat(intent).extras().string("key2").isEqualTo("value2") 

The following code snippets implement an example activityResult_DisplaysContactsPhoneNumber() test, which verifies that when a user launches a “contact” activity in the app under test, the contact phone number is displayed:

Intercepts all Intents sent to “contacts” and stubs out their responses with a valid ActivityResult, using the result code RESULT_OK

val resultData = Intent()
val phoneNumber = "123-345-6789"
resultData.putExtra("phone", phoneNumber)
val result = Instrumentation.ActivityResult(Activity.RESULT_OK, resultData) 

Instruct Espresso to provide the stub result object in response to all invocations of the “contacts” intent:

intending(toPackage("com.android.contacts")).respondWith(result)

Verify that the action used to launch the activity produces the expected stub result. In this case, the example test checks that the phone number “123–345–6789” is returned and displayed when the “contacts activity” is launched:

onView(withId(R.id.pickButton)).perform(click())
onView(withId(R.id.phoneNumber)).check(matches(withText(phoneNumber)))
Pages: 1 2 3