Writing Basic Espresso Tests

Basic instrumentated tests are fine for testing non-UI logic. They even work acceptably for some basic UI testing. The more complex your UI testing gets, though, the more likely it is that you will find plain instrumented tests to be limiting and tedious.

Espresso is designed to simplify otherwise-complex UI testing scenarios, such as:

Espresso tests are part of your instrumented tests. Espresso simply provides another API for writing test code; those tests run alongside the rest of your instrumented tests.

However, Espresso tests are very fragile. They are prone to failing not due to problems in your code but due to problems when executing the tests.

Add Espresso Dependencies

One of the androidTestImplementation dependencies that we are pulling in is androidx.test.espresso:espresso-core:

  androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

As you might expect, this contains the core Espresso code, and it is sufficient to write many Espresso tests. Additional libraries exist for testing things like RecyclerView.

Disable Animations

One of Espresso’s limitations is that it does not like the stock animated effects that Android applies to various actions, such as launching activities. You will have better results if you disable those animations.

In the “Developer options” section of the Settings app, you will want to disable:

Developer Options, Showing Disabled Animations
Developer Options, Showing Disabled Animations

Set Up the Activity or Fragment

To be able to test your UI, we need to actually have that UI appear on-screen. To that end, we can use ActivityScenario or FragmentScenario. These will launch an activity or fragment for us, allowing us to then test them afterwards.

The testListContents() test function in RosterListFragmentTest uses ActivityScenario to launch the MainActivity of this app:

  @Test
  fun testListContents() {
    ActivityScenario.launch(MainActivity::class.java)

    onView(withId(R.id.items)).check(matches(hasChildCount(3)))
  }

ActivityScenario.launch() launches the activity and returns an ActivityScenario instance. That class implements Closeable, so we can use use() to execute our tests and close the scenario (and finish the activity) when we are done.

Find Widgets via Hamcrest Matchers

Writing Espresso tests is often described as having three main steps:

  1. Find the widgets you want to examine or manipulate
  2. Perform actions on those widgets where needed (and where possible)
  3. Check to see if widgets have a certain state

Technically speaking, with Espresso, we do not “find widgets”, though it is often simplest to phrase it that way. A more accurate description would be “obtain a ViewInteraction object that pertains to a particular widget”. The ViewInteraction object in turn allows us to perform actions on the underlying widget and check the widget to see if it has a certain state.

To do that, we use an onView() static method supplied by Espresso. It will search the view hierarchy of the current activity for a widget.

How we identify the widget is via a “matcher”. A matcher simply is an object that can identify whether some other object matches certain criteria. In particular, Espresso uses Hamcrest matchers.

There are three main sources of matchers that you can use:

  1. ViewMatchers contains a number of static methods that return matchers that find a View with some specific characteristic, such as withId() to find a View with a particular ID
  2. Hamcrest’s Matchers class has a series of static methods that return matchers that help you combine other matchers (e.g., allOf() to find a View that matches more than one criteria) or work with plain Java collections (e.g., empty() to match a collection that is empty)
  3. Your own custom matchers

Here, we are using withId() to find some widget in MainActivity that has an ID of items.

Perform Actions

onView() gives us a ViewInteraction. Given a ViewInteraction, one thing that you can do is ask it to perform() one or more actions, represented by ViewAction objects. The ViewActions (note the plural) class contains a series of static methods that create ViewAction objects. And, once again, the pattern is to use static imports for those methods. Actions can include things like clicking the widget (click()), clicking the system BACK button (pressBack()), swiping in a particular direction (e.g., swipeDown()), and so on.

In this case, we want to test whether MainActivity is showing our three to-do items (from the in-memory repository) in a RecyclerView (the items widget that we found). There are no actions that we need to apply here, so we skip that step.

Assert Results

Once the activity is in the desired state via any actions, we can use the ViewInteraction from onView() to validate that the widgets contain the desired content or otherwise are set up properly. That is handled by calling check() on a ViewInteraction, passing in a ViewAssertion that… well… asserts something. A ViewAssertion basically wraps the assertion calls that you might make directly in JUnit4, working with the ViewInteraction to confirm that the underlying View has some particular state.

The simplest ViewAssertion is obtained via the matches() static method. This takes a Hamcrest matcher and confirms that the widget matches whatever the matcher’s criteria are.

One of the many static methods on ViewMatchers is hasChildCount(), which matches whether a ViewGroup (like RecyclerView) has a certain number of children. So, matches(hasChildCount(3)) is a ViewAssertion that will succeed if the items RecyclerView has three children and will fail the test if not.

The net effect of the entire test is that we set up the database with three to-do items, then launch the MainActivity and confirm that it shows those three items:

package com.commonsware.todo.ui.roster

import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.hasChildCount
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.commonsware.todo.R
import com.commonsware.todo.repo.ToDoDatabase
import com.commonsware.todo.repo.ToDoModel
import com.commonsware.todo.repo.ToDoRepository
import com.commonsware.todo.ui.MainActivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.koin.core.context.loadKoinModules
import org.koin.dsl.module

@RunWith(AndroidJUnit4::class)
class RosterListFragmentTest {
  private lateinit var repo: ToDoRepository
  private val items = listOf(
    ToDoModel("this is a test"),
    ToDoModel("this is another test"),
    ToDoModel("this is... wait for it... yet another test")
  )

  @Before
  fun setUp() {
    val context = InstrumentationRegistry.getInstrumentation().targetContext
    val db = ToDoDatabase.newTestInstance(context)
    val appScope = CoroutineScope(SupervisorJob())

    repo = ToDoRepository(db.todoStore(), appScope)

    loadKoinModules(module {
      single { repo }
    })

    runBlocking { items.forEach { repo.save(it) } }
  }

  @Test
  fun testListContents() {
    ActivityScenario.launch(MainActivity::class.java)

    onView(withId(R.id.items)).check(matches(hasChildCount(3)))
  }
}

Prev Table of Contents Next

This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.