Espresso: a nice way to test android UI journeys

Abdul Basit
5 min readJun 1, 2023

--

Today, we will be exploring Espresso; a UI test framework to write android applications UI tests. So what actually Espresso is?

Ah, you guessed it right! Espresso is brewed coffee served with fine beans but we have some different espresso in town so you might need to grab a cup of coffee while reading it through as it is going to be a long read.

So, Espresso is an open source android user interface (UI) testing framework developed by Google. Espresso is a simple, efficient and flexible testing framework.

Let’s talk about how to use Espresso to write concise and reliable UI tests.

Espresso setup.

Add following dependencies:

androidTestImplementation('androidx.test.espresso:espresso-core:3.5.1') 
androidTestImplementation('androidx.test:runner: 1.5.2')
androidTestImplementation('androidx.test:rules: 1.5.0')

Replace testInstrumentationRunner with your custom implementation of Runner in build.gradle file.

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

Espresso is purely responsible to verify the UI elements, interactions and assertions on the user interface. We can write a basic test to verify if certain element like TextView, ImageView or View/ViewGroup is visible or not. Let’s have a look:

onView(withId(R.id.textView)).check(matches(isDisplayed()))

That’s it! Yes that’s what we need write if we want to check if textView is visible on the screen. You must be curious to know that how it’s done and what this onView, check, matches are responsible for so let’s talk about them in detail.

Espresso matchers

So in order to assert that certain view is visible or gone on the UI espresso uses matchers. In espresso there are number of pre-defined matchers like withId, withText, withClassName, IsInstanceOf, IsIn that helps in verifying the interaction/assertion on the UI.
So let’s say you have an imageView with id bannerImage and you want to check if this is available on the screen or not.

onView(withId(R.id.bannerImage)).check(matches(isDisplayed()))

So what this statement is doing;

  • Looking for a view with id bannerImage through this method withId() that returns matcher.
  • matches() takes matcher that is isDisplayed and returns assertion to check method
  • on the above view (Image View with id bannerImage) espresso will perform assertion of the type isDisplayed
  • If this view will be visible on the screen then test will pass otherwise test will fail.

So there are lot of useful matchers that can be used to perform assertions like isClickable, isFocused, isChecked.

Espresso intents

Espresso uses espresso-intents to verify the intents interaction on the user interface. Let’s say you wanted to check if the opened activity is ActivityA or not. It can be easily verified through espresso intents by just writing this piece of code.

intended(hasComponent(ActivityA::class.java.simpleName))

There are other variations as well for validating intents that can be helpful to verify interactions made through intents.

Basic test on TestActivity

So we have explored a bit about espresso and how espresso matchers, intents work. Let’s implement a basic test using espresso and that too on a TestActivity of our project.
Basic setup is already defined above so let’s create androidTest folder under src folder of the project. If androidTest folder is already there then create a new File and name it TestActivityTest.kt. Once done with creating a file add these annotations on top of your class.

@RunWith(AndroidJUnit4ClassRunner::class) //similar as unit-test, used
to run test with AndroidJUnit4 runner
@LargeTest // it indicates that the test will be performing high
operations such as network, files, database.

Your class will look like this after adding those annotations.

@RunWith(AndroidJUnit4::class)
@LargeTest
class TestActivityTest { }

Let’s add your first method to this class.

@Test
fun isLauncherLogoVisible() {
onView(withId(R.id.launcherLogo)).check(matches(isDisplayed()))
}

Here’s your first test and you are ready to run this test. Tap on play button and see if your test fails or pass.

So ideally it should fail as we didn’t add Activity Scenario rule or in simple words we didn’t launched TestActivity. So we just need to add this rule on top of the class.

@get:Rule
var mActivityRule = ActivityScenarioRule(TestActivity::class.java)

Run again and now it’s time for test to turn green. Nicely done!

Ok so we have just explored only one matcher in our implementation and that is isDisplayed so let’s explore few of other matchers in RegistrationActivityTest.

Create a new file name it as RegistrationActivityTest and add required annotations as in previous test. Once done with the pre-requisite add this code snippet as your first test in this class.

@Test
fun verifyNextButtonClick() {
Espresso.onView(ViewMatchers.withId(R.id.nextButton))
.perform(ViewActions.click())
.noActivity()
Thread.sleep(400)
Espresso.onView(ViewMatchers.withId(R.id.nextButton))
.perform(ViewActions.click())
.noActivity()
Thread.sleep(400)
Espresso.onView(ViewMatchers.withId(R.id.nextButton))
.perform(ViewActions.click())
.noActivity()
Thread.sleep(400)
Espresso.onView(ViewMatchers.withId(R.id.nextButton))
.perform(ViewActions.click())
Intents.intended(IntentMatchers.hasComponent
(NextActivity::class.java.name))
}

Rather than diving into the depth of each statement in this test I will suggest you to test this on your own. And don’t forget to replace RegistrationActivity with TestActivity in your rule.

Flow Journey Test

So we have talked about writing a test for single screen with certain scenario, now let’s talk about a test where we can test complete journey of a feature. For that I am writing sign out journey test but you can explore other journeys test in the above mentioned PR.

So let’s create another file and name it JourneyTest and add required annotations on the top of the class.
But don’t add activity scenario rule in this class as we will be deciding this in our test that what activity needs to start/open. So once done with all the setup just add the below code snippet in your class.

@Test
fun verifySignOutJourney() {
ActivityScenario.launch(TestActivity::class.java)
matchSafely(R.id.launcherLogo)
matchSafely(R.id.bottomNavMenu)
Thread.sleep(SLIGHT_API_DELAY)
matchAndClick(withId(R.id.settingsIcon))
matchAndClick(withId(R.id.logOutText))
matchSafely(R.id.emailEditText)
}

This test is taking some assumptions that user is already logged in to the app and there will be no onboarding/login/signup screen after splash.

Let’s take an assumption with some screens are already made with ids mentioned below:

So at the start of this test, TestActivity will launch then test will match a view with launcherLogo id then will match for bottom navigation with id bottomNavMenu. After that test will sleep for few seconds to make sure that screen with settingsIcon is visible and then matches and tap on the image view with settingsIcon id. If test is able to find this view then it will continue it’s execution and search for logout view with id logOutText and tap on that view and in the end test will verify if the edit text is displayed with the id emailEditText (on login screen). If all these steps completes successfully then test will pass otherwise it will fail.

So we need to be mindful while writing a test to test a flow with accurate parameters and proper journey of that test. If any of unwanted screen /dialog appears the test will fail as it was not expected.

Well you must noticed that there are some methods that we haven’t seen before this section like matchesSafely, matchAndClick so yes these are utility methods that I have added to avoid boilerplate code that we need to write every time and while writing we can mainly focus on core logic.

You can explore utility methods such as matchesSafely, matchAndClick in the following gist.

That’s a wrap!

It is highly suggested to explore official documentation and give it a try by starting with a basic test.

Thank you for reading this till the end. I hope it helps and if it does then give it a clap and stay tuned!

--

--