Testing Android Activity Results

TLDR

Espresso doesn’t have a way to test ActivityResults, but it’s an important thing to test. You can do it by creating an Activity that launches and listens for your subject’s results, then using matchers against that Activity to see if your subject’s results are as expected. You can read the whole story here: https://gist.github.com/saxophone/961ceceea43f8501cbaf.

The problem

Activity results often inform an app’s behavior and is a common way to communicate between Activities. They are an important feature to test, but Espresso does not provide a simple interface for performing these tests. Who knows if Espresso will ever get there, so the burden of creating a reusable solution is on the tester.

Making a plan

One way to approach a testing solution is to have a testing-specific activity (ResultTestActivity) start the activity you want to test (SubjectActivity) and record the result code and result data in ResultTestActivity. It is then possible to write a Matcher against ResultTestActivity matching for the result code and result data from SubjectActivity. The testing flow would be as follows:

Start ResultTestActivity ResultTestActivity startsSubjectActivity TriggerSubjectActivity to finish with some result through UI actions or other ResultTestActivity receivesSubjectActivity’s results and stores them locally Use custom matchers to verify that ResultTestActivity’s stored results fromSubjectActivity are as expected

Writing the code

Step 1 is simple, define a method that creates an Intent for ResultTestActivity that stores SubjectActivity’s intent as an extra:

[code language=”java”]

private final static String EXTRA_SUBJECT_ACTIVITY_INTENT = “extraStartActivityIntent”;

public static Intent createIntent(Intent subjectIntent) {

Intent intent = new Intent(getInstrumentation().getTargetContext(),

ResultTestActivity.class);

intent.putExtra(EXTRA_SUBJECT_ACTIVITY_INTENT, subjectIntent);

intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

return intent;

}

[/code]

Step 2 involves starting SubjectActivity using subjectIntent from within ResultTestActivity’s lifecycle:

[code language=”java”]

private final static int REQUEST_CODE = 9999;

@Override

public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

Intent startActivityIntent = getIntent().getParcelableExtra(EXTRA_SUBJECT_ACTIVITY_INTENT);

startActivityForResult(startActivityIntent, REQUEST_CODE);

}

[/code]

Step 3 is specific to SubjectActivity’s behavior and what will cause it to finish with a result. Two other aspects of this process are SubjectActivity specific as well, creating subjectIntent and getting the Matcher for step 5. It is most convenient to roll these three abstractions into a single interface whose methods will be called in order to successfully execute the test:

[code language=”java”]

public interface ActivityResultTest {

/**

* @return the intent with the appropriate extras that will start

* your subject activity

*/

Intent getSubjectIntent();

/**

* Perform the necessary UI actions necessary to trigger the subject

* activity finishing with a result.

*/

void triggerActivityResult();

/**

* @return a matcher for the result test activity to match the target

* activity’s result

*/

Matcher<ResultTestActivity> getActivityResultMatcher();

}

[/code]

Step 4 is again simple, catch SubjectActivity’s result and store the data

[code language=”java”]

private int mResultCode;

private Intent mResultData;

@Override

public void onActivityResult(int requestCode, int resultCode, Intent data) {

super.onActivityResult(requestCode, resultCode, data);

if (requestCode == REQUEST_CODE) {

mResultCode = resultCode;

mResultData = data;

}

}

[/code]

Step 5 calls the last method in the previously defined interface. A very useful Matcher<ResultTestActivity> is also defined for ease of use:

[code language=”java”]

public static Matcher<ResultTestActivity>

receivedExpectedResult(final Matcher<Integer> resultCodeMatcher,

final Matcher<Intent> resultDataMatcher) {

return new TypeSafeMatcher<ResultTestActivity>() {

@Override

protected boolean matchesSafely(ResultTestActivity item) {

return resultCodeMatcher.matches(item.mResultCode) &&

resultDataMatcher.matches(item.mResultData);

}

@Override

public void describeTo(Description description) {

description.appendText(“with result code=”)

.appendDescriptionOf(resultCodeMatcher);

description.appendText(“ and with intent=”)

.appendDescriptionOf(resultDataMatcher);

}

};

}

[/code]

Putting it all together

The 5 steps can be summarized in a single static method:

[code language=”java”]

public static void runActivityResultTest(ActivityResultTest test) {

ResultTestActivity resultTestActivity = (ResultTestActivity) getInstrumentation().startActivitySync(test.getSubjectIntent());

test.triggerActivityResult();

assertThat(resultTestActivity, test.getActivityResultMatcher());

resultTestActivity.finish();

}

[/code]

Invoking this test is also simple. If Subject is an activity that should finish with a result code of RESULT_OK and data that holds a String named “RESULT_STRING” with a value of “resultString”, the test becomes:

[code language=”java”]

@Test

public static void subjectActivityShouldReturnCorrectActivityResult() {

runActivityResultTest(new ActivityResultTest() {

@Override

public Intent getSubjectIntent() {

return new Intent(getInstrumentation().getTargetContext(), SubjectActivity.class);

}

@Override

public void triggerActivityResult() {

// Perform the appropriate actions necessary to trigger

// SubjectActivity’s result

}

@Override

public Matcher<ResultTestActivity> getActivityResultMatcher() {

return receivedExpectedResult(is(RESULT_OK),

IntentMatchers.hasExtra(“RESULT_STRING”, “resultString”));

}

});

}

[/code]

Caveats

This looks like it will work as-is but there’s a high chance it won’t. Enabling the “Don’t keep activities” flag in Developer options on your test device will cause two problems. Once your subject finishes, ResultTestActivity will launch the subject’s intent again, which is easily resolved by surrounding said logic in a null check for savedInstanceState. The harder problem to solve is that the ResultTestActivity returned from startActivitySync() isn’t the same object that is receiving the result because it got recreated. Instead of using that object, we have to get the current activity on the stack and test against that.

These enhancements are reflected in the whole story, which you can read here https://gist.github.com/saxophone/961ceceea43f8501cbaf.

Happy testing!