Pluralsight Logo
Author avatar

Pavneet Singh

Author badge Author

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

Pavneet Singh

Author BadgeAuthor
  • Jul 23, 2018
  • 16 Min read
  • 277 Views
  • Jul 23, 2018
  • 16 Min read
  • 277 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
2
3
4
5
6
// click on 3rd position
// where the type of data source is string
onData(allOf(is(instanceOf(String.class)))).atPosition(2).perform(click());

// select view with text "item 3"
onData(allOf(is(instanceOf(String.class)), is("item 3"))).perform(click());

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

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

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.

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Rule
public ActivityTestRule<MainActivity> activityTestRule =
  new ActivityTestRule<MainActivity>(MainActivity.class,true,false /*lazy launch activity*/){
    @Override
    protected Intent getActivityIntent() {
      /*added predefined intent data*/
      Intent intent = new Intent();
      intent.putExtra("key","value");
      return intent;
    }
  };

@Test
public void customizeIntent(){
    // note instead of null, an intent object can be passed
    activityTestRule.launchActivity(null);
}

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

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
2
3
4
5
6
7
8
9
10
@Rule
public ActivityTestRule<NewsActivity> activityTestRule =
        new ActivityTestRule<>(NewsActivity.class);


@Before
public void init(){
    Context context = InstrumentationRegistry.getTargetContext();
    SomeLib.initialize(context); // like fresco, firebase etc
}

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.pavneet_singh;

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.matcher.RootMatchers.withDecorView;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.withHint;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import android.support.test.espresso.contrib.RecyclerViewActions;
import android.support.test.filters.LargeTest;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import com.pavneet_singh.espressotestingdemo.R;

@RunWith(AndroidJUnit4.class)
public class MainActivityTest {
  // To launch the mentioned activity under testing
  @Rule
  public ActivityTestRule<MainActivity> mActivityRule =
      new ActivityTestRule<>(MainActivity.class);

  @Test
  public void testButtonClick() {
    // enter name
    onView(withId(R.id.editTextName)).perform(typeText("Pavneet"), closeSoftKeyboard());
    // clear text
    onView(withText("Clear")).perform(click());

    // check hint visibility after the text is cleared
    onView(withId(R.id.editTextName)).check(matches(withHint("Enter Name")));
    onView(withId(R.id.editTextName)).check(matches(isDisplayed()));
    onView(withId(R.id.editTextName))
      .perform(RecyclerViewActions.actionOnItemAtPosition(3, click()));
    // check toast visibility
    onView(withText("Pavneet Toast")).
      inRoot(withDecorView(not(is(activity.getWindow().getDecorView())))).
      check(matches(isDisplayed()));
    }
}

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
2
// check view exists in current layout hierarchy
onView(withId(R.id.view_id)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)));

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
2
3
4
5
6
7
8
9
@Rule
public ActivityTestRule<NewsActivity> activityTestRule =
  new ActivityTestRule<>(NewsActivity.class);

@Before
public void yourSetUPFragment() {
  activityTestRule.getActivity()
    .getFragmentManager().beginTransaction();
}

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.

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

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:

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

    @Override
    public void perform(UiController uiController, View view) {
        ((TextView) view).setText(value);
    }

    @Override
    public String getDescription() {
        return "replace text";
    }
  };
}

Then it can be applied as:

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

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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class FontSizeMatcher extends TypeSafeMatcher<View> {
  // field to store values
  private final float expectedSize;

  public FontSizeMatcher(float expectedSize) {
    super(View.class);
    this.expectedSize = expectedSize;
  }

  @Override
  protected boolean matchesSafely(View target) {
    // stop executing if target is not textview
    if (!(target instanceof TextView)){
        return false;
    }
    // target is a text view so apply casting then retrieve and test the desired value
    TextView targetEditText = (TextView) target;
    return targetEditText.getTextSize() == expectedSize;
  }

  @Override
  public void describeTo(Description description) {
      description.appendText("with fontSize: ");
      description.appendValue(expectedSize);
  }
}

Then apply the testing as:

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

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:

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

Test to stub and verify the triggered intent:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Intents.init(); \\ required for intent stubbing

// create a desired matcher with action as Intent.ACTION_VIEW
Matcher<Intent> expectedIntent = hasAction(Intent.ACTION_VIEW);

// return stub result when the intent is fired
intending(expectedIntent).respondWith(new Instrumentation.ActivityResult(0, null));

// click the button to fire the `onClick` to open url in browser
onView(withText(R.string.details_story_link)).perform(click());

// validate the previously fired intent and match it against the desired intent
intended(expectedIntent);

// clears the intent state
Intents.release();

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):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class EspressoTestingIdlingResource {
  private static final String RESOURCE = "GLOBAL";

  private static CountingIdlingResource mCountingIdlingResource =
    new CountingIdlingResource(RESOURCE);

  public static void increment() {
    mCountingIdlingResource.increment();
  }

  public static void decrement() {
    mCountingIdlingResource.decrement();
  }

  public static IdlingResource getIdlingResource() {
    return mCountingIdlingResource;
  }
}
  • Now, you just need to invoke the increment() method, just before the execution of background process:
1
EspressoTestingIdlingResource.increment();
  • Invoke the decrement() method, after the background task has been finished:
1
EspressoTestingIdlingResource.decrement();
  • Finally, register the idling resource into your test class:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Rule
public ActivityTestRule<NewsActivity> activityTestRule =
  new ActivityTestRule<>(NewsActivity.class);

@Before
public void registerIdlingResource() {
  // let espresso know to synchronize with background tasks
  IdlingRegistry.getInstance().register(EspressoTestingIdlingResource.getIdlingResource());
}

@After
public void unregisterIdlingResource() {
  IdlingRegistry.getInstance().unregister(EspressoTestingIdlingResource.getIdlingResource());
}

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

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

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.

1