Overview:

This is the first of a multi-part blog series exploring Espresso for Android. In this first post, I will discuss the basics of Espresso and how to get started using the UI Automation testing tool using a full example.

What is Espresso?

Espresso is a testing tool released by Google to help developers write reliable and automated UI tests. Compared to other Android user interface (UI) testing tools, Espresso provides synchronization of test actions with the UI. This means that Espresso knows when the UI thread of an application is idle, and will run test code at the appropriate times. Previously, other workarounds were needed (e.g. sleeping for a certain amount of time before executing the next instruction). Since the release of Espresso v2.0, the testing framework is available as part of the Android Support Repository and now supports JUnit4.

Setting up Espresso for your project

First we need to install the Android Testing Support Library.

  1. Open up the SDK manager
    • Tools -> Android -> SDK Manager
  2. Select Android Support Repository and install



In your build.gradle file add these dependencies:
dependencies {
 androidTestCompile 'com.android.support.test:runner:0.4'
 // Set this dependency to use JUnit 4 rules
 androidTestCompile 'com.android.support.test:rules:0.4'
 // Set this dependency to build and run Espresso tests
 androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1'
 // Set this dependency to build and run UI Automator tests
 androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.2'
}

Also add this to same file:
android {
 defaultConfig {
 testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
 }
}

*Note: you may have seen both testCompile and androidTestCompile in the gradle dependencies section. testCompile is the configuration for unit tests located under src/test and also runs off the Java JVM. On the other hand, androidTestCompile is for tests located under src/androidTest and runs off an Android device or emulator.


On your test device, a few settings need to be changed or you may experience unexpected results or test failures. Navigate to Developer Options and turn off:

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

Writing Espresso Tests

Espresso tests are based on a formula:

 onView(ViewMatcher).perform(ViewAction).check(ViewAssertion);

Let's break this down.

ViewMatchers - Class that contains a set of hamcrest matchers that selects a certain view
http://developer.android.com/reference/android/support/test/espresso/matcher/ViewMatchers.html
Examples:

  • withId(R.id.my_textview)
  • withText("Login")
  • withText(startsWith("Lo"))

*Note: Hamcrest matchers dig through the view hierarchy, and lets you find a view that matches a certain view rather than directly referencing the view (think findViewById()). In Espresso, only immediately visible views are considered.

ViewActions - Class that provides helper methods to perform UI actions
http://developer.android.com/reference/android/support/test/espresso/action/ViewActions.html

  • typeText("ABCDEFG")
  • click()

ViewAssertions - Class that provides helper methods to assert a view state
http://developer.android.com/reference/android/support/test/espresso/assertion/ViewAssertions.html
Examples:

  • doesNotExist()
  • matches(withText("Did the text change?"))

Using a combination of these classes allow you to construct Espresso tests. Let's dive into an example.

Espresso Example

  1. Create a new project in Android Studio. Make a blank activity called MainActivity, along with its layout file activity_main.xml.

  2. Modify the build.gradle file under the app module (app/build.gradle) to match the following:

    apply plugin: 'com.android.application'
    
    android {
     compileSdkVersion 23
     buildToolsVersion "23.0.0"
    
     defaultConfig {
     applicationId "com.captech.jhong.expressoexample"
     minSdkVersion 15
     targetSdkVersion 23
     versionCode 1
     versionName "1.0"
     testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
     }
     buildTypes {
     release {
     minifyEnabled false
     proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
     }
     }
    }
    
    dependencies {
     compile 'com.android.support:appcompat-v7:23.0.1'
     androidTestCompile 'com.android.support.test:runner:0.4'
     androidTestCompile 'com.android.support.test:rules:0.4'
     androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1'
    }
    

*Note: applicationId can differ based on your setup

Modify activity_main.xml to have the following views:
  • 1x Toolbar
  • 1x TextView
  • 1x EditText
  • 2x Buttons (one button to change the text of the Toolbar, the other to change the text of the TextView)

Feel free to use the layout below or create your own



 

 

Make sure to adjust styles.xml to have no action bar and add color

Modify the MainActivity
package com.captech.jhong.expressoexample;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity implements OnClickListener {

 Button mSetToolbarButton, mSetTextViewButton;
 Toolbar mToolbar;
 EditText mEditText;
 TextView mTextView;

 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_main);
 initToolbar();

 mTextView = (TextView) findViewById(R.id.activity_main_textview);
 mEditText = (EditText) findViewById(R.id.activity_main_edit_text);
 mSetToolbarButton = (Button) findViewById(R.id.set_toolbar_title_button);
 mSetTextViewButton = (Button) findViewById(R.id.set_textview_button);
 mSetToolbarButton.setOnClickListener(this);
 mSetTextViewButton.setOnClickListener(this);
 }

 @Override
 public boolean onCreateOptionsMenu(Menu menu) {
 // Inflate the menu; this adds items to the action bar if it is present.
 getMenuInflater().inflate(R.menu.menu_main, menu);
 return true;
 }

 @Override
 public boolean onOptionsItemSelected(MenuItem item) {
 // Handle action bar item clicks here. The action bar will
 // automatically handle clicks on the Home/Up button, so long
 // as you specify a parent activity in AndroidManifest.xml.
 int id = item.getItemId();

 //noinspection SimplifiableIfStatement
 if (id == R.id.action_settings) {
 return true;
 }

 return super.onOptionsItemSelected(item);
 }

 private void initToolbar() {

 mToolbar = (Toolbar) findViewById(R.id.activity_main_toolbar);
 if (mToolbar != null) {
 setSupportActionBar(mToolbar);
 }

 }

 @Override
 public void onClick(View view) {
 String editTextString = mEditText.getText().toString();

 switch (view.getId()){
 case R.id.set_toolbar_title_button:
 mToolbar.setTitle(editTextString);
 break;
 case R.id.set_textview_button:
 mTextView.setText(editTextString);
 break;
 default:
 break;
 }
 }
}

Finally time to write the tests! Create an empty Java class under src/androidTest and call it "MainActivityTests"

Include the annotation for JUnit4:


package com.captech.jhong.expressoexample;

import android.support.test.runner.AndroidJUnit4;

import org.junit.runner.RunWith;

@RunWith(AndroidJUnit4.class)
public class MainActivityTest {
}

Next, we include the activity test rule. This is essentially the activity we will be testing on.
http://developer.android.com/reference/android/support/test/rule/ActivityTestRule.html

 @RunWith(AndroidJUnit4.class)
 public class MainActivityTest {

 @Rule
 public ActivityTestRule mActivityRule = new ActivityTestRule<>(MainActivity.class);

 }

Let's begin by testing the functionality of the TextView text-changing button. We will name the function "testTextViewTextChanged" and annotate that it is a test.

 @Test
 public void testTextViewTextChanged() {

 }

In order to determine whether the TextView text has changed the test should enter a string into the EditText, press the correct button, and verify that the text matches the text entered into the EditText. Using the ViewMatcher withId(), we can locate the EditText like so:

 onView(withId(R.id.activity_main_edit_text))

This returns a ViewInteraction object that we will perform actions on. Next, the string to be typed needs to be determined. There are two options:
  1. Hardcode the string ("TextView Test String")
  2. Store the string in strings.xml in your project and reference it in your test

In this example, the second option will be explored. This option, although will take some more time to set up, will save time in the long run for more complex projects. Instead of rewriting tests whenever strings need to be changed, the tester can simply modify the strings.xml file.

Luckily, the Android Instrumentation API allows us to grab the context. With the context in hand, we can grab the necessary string.

 String title = InstrumentationRegistry.getTargetContext().getString(R.string.test_textview);
 onView(withId(R.id.activity_main_edit_text)).perform(typeText(title), closeSoftKeyboard());


After typing in the string, the button must be pushed.
 onView(withId(R.id.set_textview_button)).perform(click());

Finally, we can verify the results.
 @Test
 public void testTextViewTextChanged() {
 String text = InstrumentationRegistry.getTargetContext().getString(R.string.test_textview);

 onView(withId(R.id.activity_main_edit_text)).perform(typeText(text), closeSoftKeyboard());
 onView(withId(R.id.set_textview_button)).perform(click());
 onView(withId(R.id.activity_main_textview)).check(matches(withText(text)));

 }

The second test is a little different. We do not have the access to the Toolbar as one might think. In fact, if you copy the previous test and just swap out the id of the TextView for the Toolbar, the test will fail! This is due to the Toolbar containing a TextView, which in turn has the string that we want to verify. Therefore, a slightly different approach is necessary.

 @Test
 public void testToolbarTextChanged() {
 String title = InstrumentationRegistry.getTargetContext().getString(R.string.test_toolbar);

 onView(withId(R.id.activity_main_edit_text)).perform(typeText(title), closeSoftKeyboard());
 onView(withId(R.id.set_toolbar_title_button)).perform(click());
 onView(allOf(isAssignableFrom(TextView.class), withParent(isAssignableFrom(Toolbar.class))))
 .check(matches(withText(title)));
 }

This ViewMatcher looks for all the TextViews visible on the screen and narrows the search even further by looking for views that have a parent view of Toolbar.

Optimization

We can make the above code slightly more concise by delegating some of the repeated functionality to separate methods. In the example,

onView(withId(R.id.activity_main_edit_text)).perform(typeText(string), closeSoftKeyboard());

is repeated twice (once in each method). We can put this into its own method as such:
private static ViewInteraction setUpEditText(String string){
 return onView(withId(R.id.activity_main_edit_text)).perform(typeText(string), closeSoftKeyboard());
}

MainActivityTests.java
package com.captech.jhong.expressoexample;

import android.support.test.InstrumentationRegistry;
import android.support.test.espresso.ViewInteraction;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.support.v7.widget.Toolbar;
import android.widget.TextView;

import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.action.ViewActions.closeSoftKeyboard;
import static android.support.test.espresso.action.ViewActions.typeText;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withParent;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf;

@RunWith(AndroidJUnit4.class)
public class MainActivityTests {

 @Rule
 public ActivityTestRule mActivityRule = new ActivityTestRule<>(MainActivity.class);

 @Test
 public void testToolbarTextChanged() {
 String title = InstrumentationRegistry.getTargetContext().getString(R.string.test_toolbar);
 setUpEditText(title);
 onView(withId(R.id.set_toolbar_title_button)).perform(click());
 onView(allOf(isAssignableFrom(TextView.class), withParent(isAssignableFrom(Toolbar.class))))
 .check(matches(withText(title)));
 }

 @Test
 public void testTextViewTextChanged(){
 String text = InstrumentationRegistry.getTargetContext().getString(R.string.test_textview);
 setUpEditText(text);
 onView(withId(R.id.set_textview_button)).perform(click());
 onView(withId(R.id.activity_main_textview)).check(matches(withText(text)));

 }

 private static ViewInteraction setUpEditText(String string){
 return onView(withId(R.id.activity_main_edit_text)).perform(typeText(string), closeSoftKeyboard());
 }

}

To run this test in Android Studio, right click and select Run MainActivityTests.


Download the project on Github:

Conclusion

Espresso is an efficient UI Automation testing tool for Android with a wide variety of capabilities. This introduction to Espresso covered basic functionality and examples. From this tutorial you learned to construct basic UI tests on TextViews. What happens when the situation isn't so simple? Stay tuned for the second chapter: Using Espresso with AdapterViews and RecyclerViews.