UI Testing (Espresso)

The previous lesson's unit tests mocked away the Repository entirely. Espresso tests the opposite way: they drive the real RecipeBox app, running on a real device or emulator, by simulating an actual user tapping and typing on real screens.

Adding Espresso and the androidTest Source Set

// app/build.gradle
dependencies {
    androidTestImplementation 'androidx.test.ext:junit:1.2.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
}
Explanation: These tests live in src/androidTest/java — a different source set from the src/test/java unit tests in the previous lesson — because they need Gradle's connectedAndroidTest task to install the debug APK onto a device or emulator and run it there.

onView, withId, perform, check

Every Espresso test follows the same three-part shape: find a view, act on it, then assert something about the result.

onView(matcher) → find a View, e.g. withId(R.id.etTitle) .perform(action) → act on it, e.g. typeText("Pancakes"), click() .check(assertion) → verify state, e.g. matches(isDisplayed())

Worked Example: Adding a Recipe

This test drives RecipeBox's add-recipe form exactly as a user would: type a title, type prep minutes, tap save, then confirm the new recipe shows up in the list.

@RunWith(AndroidJUnit4.class)
public class AddRecipeTest {

    @Rule
    public ActivityScenarioRule<AddRecipeActivity> activityRule =
            new ActivityScenarioRule<>(AddRecipeActivity.class);

    @Test
    public void savingRecipe_showsItInList() {
        onView(withId(R.id.etTitle))
            .perform(typeText("Pancakes"), closeSoftKeyboard());

        onView(withId(R.id.etPrepMinutes))
            .perform(typeText("15"), closeSoftKeyboard());

        onView(withId(R.id.btnSave))
            .perform(click());

        onView(withText("Pancakes"))
            .check(matches(isDisplayed()));
    }
}
Explanation:
  • withId(R.id.etTitle) and withText("Pancakes") come from ViewMatchers; typeText(), click(), and closeSoftKeyboard() come from ViewActions — both are typically static-imported at the top of the test class.
  • ActivityScenarioRule launches AddRecipeActivity fresh before this test and tears it down afterward, so tests don't interfere with each other.
  • The final check(matches(isDisplayed())) confirms the saved recipe's title actually appears on screen — proving the whole save-and-refresh flow worked, not just that btnSave was clickable.

IdlingResource for Asynchronous Work

Espresso automatically waits for the main thread to go idle between steps, but it has no way to know about work happening elsewhere — such as RecipeBox's Repository saving to Room on a background thread before the list refreshes. Without help, the test can race ahead and check the list before the save finishes, causing flaky failures.

An IdlingResource is how a test tells Espresso "wait — there's still async work in flight." Registering one around RecipeBox's save operation makes Espresso pause the test until the Repository reports it's idle again, before continuing to the next onView(...) call.

Espresso vs Unit Tests

QuestionPrefer
Is this validation/calculation logic correct?Unit test (fast, no device)
Does tapping Save actually update the screen?Espresso (real UI, real device)
Does the Repository call the right DAO method?Unit test with Mockito
Does the whole add-recipe flow work end-to-end?Espresso
A healthy test suite leans heavily on fast unit tests, with a smaller number of Espresso tests covering the most important user flows — Espresso tests are slower and more brittle to UI changes, so they're not meant to cover every branch of logic.
Espresso needs a real device or emulator for every run. The next lesson, Robolectric, shows a middle ground: real Android framework behavior, running on the plain JVM.