Author avatar

Pavneet Singh

Testing with Espresso - Part 2 (Espresso and Edge Cases)

Pavneet Singh

  • Jul 23, 2018
  • 16 Min read
  • 23,544 Views
  • Jul 23, 2018
  • 16 Min read
  • 23,544 Views
Espresso
Android
Testing

Espresso and Edge Cases

The previous article Setup and Basics covered the basics of testing and setting up environment.The next step is to test various techniques to test various android views like toast, fonts, intent, network calls etc. This tutorial covers the edge cases (like run time permissions,activity result etc) to work with espresso testing.

Testing ListView or RecyclerView

ListView or Spinner uses AdapterView that displays the data at run time from the adapter. So, as opposed to other views, adapterview does not display all the list items at the same time which means that onView would not find views that are not currently loaded.

onData() is specifically applied to bring the desired list item into focus before performing any operation on the given position.

1// click on 3rd position
2// where the type of data source is string
3onData(allOf(is(instanceOf(String.class)))).atPosition(2).perform(click());
4
5// select view with text "item 3"
6onData(allOf(is(instanceOf(String.class)), is("item 3"))).perform(click());
java

To perform click on RecyclerView. espresso-contrib dependency is required which provides RecyclerView specific methods as shown below:

1// verify the visibility of recycler view on screen
2onView(withId(R.id.news_frag_recycler_list)).check(matches(isDisplayed()));
3// perform click on view at 3rd position in RecyclerView
4onView(withId(R.id.news_frag_recycler_list))
5  .perform(RecyclerViewActions.actionOnItemAtPosition(3, click()));
java

Starting an Activity with Customize Intent

To start an activity, an Intent object is required by the ActivityTestRule instance and the overloaded version of ActivityTestRule is applied.

1ActivityTestRule(SingleActivityFactory<T> activityFactory, boolean initialTouchMode, boolean launchActivity)
java

Where launchActivity is false means the activity will not be launched automatically by the test. See the implementation:

1@Rule
2public ActivityTestRule<MainActivity> activityTestRule =
3  new ActivityTestRule<MainActivity>(MainActivity.class,true,false /*lazy launch activity*/){
4    @Override
5    protected Intent getActivityIntent() {
6      /*added predefined intent data*/
7      Intent intent = new Intent();
8      intent.putExtra("key","value");
9      return intent;
10    }
11  };
12
13@Test
14public void customizeIntent(){
15    // note instead of null, an intent object can be passed
16    activityTestRule.launchActivity(null);
17}
java

Handling Marshmallow RunTime Permission Model

To enhance data and crucial resource security, Android Marshmallow and above inform the user about the resources used by app while the application is running. Some of the resources will be storage, contacts, location, and other hardware resources like sensors, camera etc.

So for testing, runtime permissions need to be granted first and preferably in methods annotated with @Before annotation:

1@Before
2public void grantPhonePermission() {
3  // In M+, trying to call a number will trigger a runtime dialog. Make sure
4  // the permission is granted before running this test.
5  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
6    getInstrumentation().getUiAutomation().executeShellCommand(
7      "pm grant " + getTargetContext().getPackageName()
8      + " android.permission.READ_EXTERNAL_STORAGE");
9  }
10}
java

Using Context for Initialization

Often a context instance is required to setup libraries before their usage, so the context instance can be retrieved from InstrumentationRegistry instance.

1@Rule
2public ActivityTestRule<NewsActivity> activityTestRule =
3        new ActivityTestRule<>(NewsActivity.class);
4
5
6@Before
7public void init(){
8    Context context = InstrumentationRegistry.getTargetContext();
9    SomeLib.initialize(context); // like fresco, firebase etc
10}
java

Testing Toast Visibility

Toasts are floating messages, shown to users over the current screen. To customize the appearance of toast, use the below code and carefully pay attention to the import statement.

1package com.pavneet_singh;
2
3import static android.support.test.espresso.Espresso.onView;
4import static android.support.test.espresso.action.ViewActions.click;
5import static android.support.test.espresso.action.ViewActions.closeSoftKeyboard;
6import static android.support.test.espresso.action.ViewActions.typeText;
7import static android.support.test.espresso.matcher.RootMatchers.withDecorView;
8import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
9import static android.support.test.espresso.matcher.ViewMatchers.withHint;
10import static android.support.test.espresso.matcher.ViewMatchers.withId;
11import static android.support.test.espresso.assertion.ViewAssertions.matches;
12import static android.support.test.espresso.matcher.ViewMatchers.withText;
13import android.support.test.espresso.contrib.RecyclerViewActions;
14import android.support.test.filters.LargeTest;
15import android.support.test.rule.ActivityTestRule;
16import android.support.test.runner.AndroidJUnit4;
17import org.junit.Rule;
18import org.junit.Test;
19import org.junit.runner.RunWith;
20import com.pavneet_singh.espressotestingdemo.R;
21
22@RunWith(AndroidJUnit4.class)
23public class MainActivityTest {
24  // To launch the mentioned activity under testing
25  @Rule
26  public ActivityTestRule<MainActivity> mActivityRule =
27      new ActivityTestRule<>(MainActivity.class);
28
29  @Test
30  public void testButtonClick() {
31    // enter name
32    onView(withId(R.id.editTextName)).perform(typeText("Pavneet"), closeSoftKeyboard());
33    // clear text
34    onView(withText("Clear")).perform(click());
35
36    // check hint visibility after the text is cleared
37    onView(withId(R.id.editTextName)).check(matches(withHint("Enter Name")));
38    onView(withId(R.id.editTextName)).check(matches(isDisplayed()));
39    onView(withId(R.id.editTextName))
40      .perform(RecyclerViewActions.actionOnItemAtPosition(3, click()));
41    // check toast visibility
42    onView(withText("Pavneet Toast")).
43      inRoot(withDecorView(not(is(activity.getWindow().getDecorView())))).
44      check(matches(isDisplayed()));
45    }
46}
java

Check View's Visibility in ScrollView

ScrollView can host multiple views (EditText, Buttons, CheckBox, etc.) vertically or horizontally, so it is possible that some views are not currently visible on the screen. Hence applying matches(isDisplayed()) cannot be applied on views which are not visible.

To test those views which are not visible on the current window, use withEffectiveVisibility:

1// check view exists in current layout hierarchy
2onView(withId(R.id.view_id)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)));
java

Test Fragment Using Espresso

Your Activity (Screen) can be divided into smaller containers called Fragments. During testing you need to first add fragments into the host activity which can either be done before starting the actual test or later, when it is required.

1@Rule
2public ActivityTestRule<NewsActivity> activityTestRule =
3  new ActivityTestRule<>(NewsActivity.class);
4
5@Before
6public void yourSetUPFragment() {
7  activityTestRule.getActivity()
8    .getFragmentManager().beginTransaction();
9}
java

Here @Before function will be executed before starting the test. It’s like a setup function to load the fragment into the host activity which is appropriate when test is only fragment oriented.

This transaction can also be done later whenever required.

Note : You can also create normal fragment object and add them to host activity using the fragment transaction.

Creating Customize ViewAction

One of the common practice is to change the text in TextView but the regular approach of typing text in EditText cannot be applied on TextView, so the below approach cannot be used.

1onView(withId(R.id.editText)).perform(typeText("my text"), closeSoftKeyboard());
java

The above will fail because the input method editor (IME) is not supported by TextView, meaning that user cannot input values into TextView via the keyboard. To type text into TextView, a customize ViewAction required.

Typing Text into TextView Using Customize ViewAction

ViewAction is an abstract class, so to create a customize ViewAction an anonymous class of ViewAction type is created:

1public static ViewAction setTextInTextView(final String value){
2  return new ViewAction() {
3    @SuppressWarnings("unchecked")
4    @Override
5    public Matcher<View> getConstraints() {
6      return allOf(isDisplayed(), isAssignableFrom(TextView.class));
7//                                            ^^^^^^^^^^^^^^^^^^^
8// To check that the found view is TextView or it's subclass like EditText
9// so it will work for TextView and it's descendants  
10    }
11
12    @Override
13    public void perform(UiController uiController, View view) {
14        ((TextView) view).setText(value);
15    }
16
17    @Override
18    public String getDescription() {
19        return "replace text";
20    }
21  };
22}
java

Then it can be applied as:

1onView(withId(R.id.textview_id))
2  .perform(setTextInTextView("text input"));
java

Check Attributes Like FontSize

Espresso does not have any matcher to test the font size. This and many other attribute values can be matched though a customized matcher created for particular scenarios like this, as shown below.

  • Create a customize TypeSafeMatcher:
1public class FontSizeMatcher extends TypeSafeMatcher<View> {
2  // field to store values
3  private final float expectedSize;
4
5  public FontSizeMatcher(float expectedSize) {
6    super(View.class);
7    this.expectedSize = expectedSize;
8  }
9
10  @Override
11  protected boolean matchesSafely(View target) {
12    // stop executing if target is not textview
13    if (!(target instanceof TextView)){
14        return false;
15    }
16    // target is a text view so apply casting then retrieve and test the desired value
17    TextView targetEditText = (TextView) target;
18    return targetEditText.getTextSize() == expectedSize;
19  }
20
21  @Override
22  public void describeTo(Description description) {
23      description.appendText("with fontSize: ");
24      description.appendValue(expectedSize);
25  }
26}
java

Then apply the testing as:

1onView(withId(R.id.id_of_view)).check(matches(withFontSize(36)));
java

Testing Intent, IntentAction, and ActivityResult

Intent can be used to start an already installed app to handle tasks like opening links with a default browser:

1public void onClick(View view) {
2    Intent intent = new Intent(Intent.ACTION_VIEW);
3    intent.setData(Uri.parse("https://www.pluralsight.com/guides/author/Pavneet-Sing"));
4    if (ActivityUtils.resolveIntent(intent,getActivity()))
5        startActivity(intent);
6    }
java

Test to stub and verify the triggered intent:

1Intents.init(); \\ required for intent stubbing
2
3// create a desired matcher with action as Intent.ACTION_VIEW
4Matcher<Intent> expectedIntent = hasAction(Intent.ACTION_VIEW);
5
6// return stub result when the intent is fired
7intending(expectedIntent).respondWith(new Instrumentation.ActivityResult(0, null));
8
9// click the button to fire the `onClick` to open url in browser
10onView(withText(R.string.details_story_link)).perform(click());
11
12// validate the previously fired intent and match it against the desired intent
13intended(expectedIntent);
14
15// clears the intent state
16Intents.release();
java

Testing Third-party Network Calls

Android supports many popular network call APIs like retrofit, okhttp,volley, etc. to access server data. Network calls are asynchronous and, during testing, Espresso is unware of the idle time required to finish. Hence, Espresso cannot provide the synchronization guarantees in those situations. In order to make Espresso aware of your app's long-running processes, an idling resource is used to keep track of idle time to access the data from the server and, later, to display data on screen for testing the views.

Steps :

  • Create Espresso idling resource in java package (not any testing package):
1public class EspressoTestingIdlingResource {
2  private static final String RESOURCE = "GLOBAL";
3
4  private static CountingIdlingResource mCountingIdlingResource =
5    new CountingIdlingResource(RESOURCE);
6
7  public static void increment() {
8    mCountingIdlingResource.increment();
9  }
10
11  public static void decrement() {
12    mCountingIdlingResource.decrement();
13  }
14
15  public static IdlingResource getIdlingResource() {
16    return mCountingIdlingResource;
17  }
18}
java
  • Now, you just need to invoke the increment() method, just before the execution of background process:
1EspressoTestingIdlingResource.increment();
java
  • Invoke the decrement() method, after the background task has been finished:
1EspressoTestingIdlingResource.decrement();
java
  • Finally, register the idling resource into your test class:
1@Rule
2public ActivityTestRule<NewsActivity> activityTestRule =
3  new ActivityTestRule<>(NewsActivity.class);
4
5@Before
6public void registerIdlingResource() {
7  // let espresso know to synchronize with background tasks
8  IdlingRegistry.getInstance().register(EspressoTestingIdlingResource.getIdlingResource());
9}
10
11@After
12public void unregisterIdlingResource() {
13  IdlingRegistry.getInstance().unregister(EspressoTestingIdlingResource.getIdlingResource());
14}
java

IdlingResource is not required for AsyncTask, From docs By default, Espresso synchronizes all view operations with the UI thread as well as AsyncTasks.

Espresso Test Recorder

Android studios provide an Espresso test recorder which allows to recording of the user(tester) event on the real app and then converts those events into Espresso test cases.

To start recording a test with Espresso Test Recorder, proceed as follows:

  1. Click Run > Record Espresso Test.
  2. In the Select Deployment Target window, choose the device on which you want to record the test, then Click OK.
  3. A build is triggered to compile and will launch the app along with an activity record panel to record user interaction for Espresso test generation.

Coverage

Code coverage is a concept which represents the percentage of code, covered by the tests. To run test coverage:

  • Go to navigation and open the test class
  • Either Goto Run option in menu and select Run "Name of your test" with Coverage option or right click on class name and choose run with coverage.

You can also define a location in module(app) gradle file to save the reposts as

1android {
2  ...
3  // Encapsulates options for running tests.
4  testOptions {
5    // Changes the directory where Gradle saves test reports. By default, Gradle saves test reports
6    // in the path_to_your_project/module_name/build/outputs/reports/ directory.
7    // '$rootDir' sets the path relative to the root directory of the current project.
8    reportDir "$rootDir/test-reports"
9    // Changes the directory where Gradle saves test results. By default, Gradle saves test results
10    // in the path_to_your_project/module_name/build/outputs/test-results/ directory.
11    // '$rootDir' sets the path relative to the root directory of the current project.
12    resultsDir "$rootDir/test-results"
13  }
14}
groovy

Reference


I hope that this guide will now help you to be a stellar programmer. You can share your love by giving this guide a thumbs up.