ActivityTestRule: Espresso’s Test “Lifecycle”

The purpose of this post is to shed some light onto the order of operations for test cases written using Espresso’s new ActivityTestRule. Specifically, discussing when methods like beforeActivityLaunched(), afterActivityLaunched(), and afterActivityFinished() are called relative to both the test and activity lifecycle.

History Lesson: ActivityInstrumentationTestCase2

First, the old way of instrumentation testing, ActivityInstrumentationTestCase2. In previous iterations of Espresso, 2.0 and earlier, a test case might look something like this:


public class MyOldTest extends ActivityInstrumentationTestCase2<MyActivity> {
public MyOldTest() {
super(MyActivity.class);
}
@Override
protected void onSetUp(Context context) {
super.onSetUp(context);
// Do all my test set up here
Intent myCustomIntent = new Intent();
// Add extras and stuff
// Maybe prepare some mock service calls
// Maybe override some depency injection modules with mocks
setActivityIntent(myCustomIntent);
getActivity(); // launch activity under test
}
@Override
protected void tearDown() {
// destroy stuff
}
public void testStuff() {
// Verify Oscar Grouch is grouchy
}
}

view raw

MyOldTest.java

hosted with ❤ by GitHub

With the important takeaways being:

  • All the ‘set up’ is done in the onSetUp method (Mock services, mock data, custom intents).
  • The method getActivity must be called to launch the Activity under test.
  • Writing a new test case for the same activity with a different setup often requires a new test class.

In addition, the test “lifecycle” is reasonably well defined:
ActivityInstrumentationLifecycle (1)

Saying there is a well defined test “lifecycle” is valid because:

  1.  onSetUp will always be called before getActivity()
  2. getActivity() will always come before the activity creation
  3. Activity creation will always come before test execution
  4. and etc… per above diagram

And it’s an important history lesson because the same assumptions can not be made for the new ActivityTestRule. In fact, several new rules, some of which are not entirely obvious, apply. Therefore, it becomes helpful to understand the differences described below before migrating legacy tests to the new ActivityTestRule structure.

The ActivityTestRule “Lifecycle”

In a successful effort to reduce ‘boilerplate’ (setup code) in test classes the Espresso developers introduced the ActivityTestRule. As a result, methods like onSetUp and getActivity were removed from test classes and brought into the new ActivityTestRule class. Allowing developers to write tests like,


@RunWith(AndroidJUnit4.class)
public MyNewTest {
@Rule
public MyCustomRule<MyActivity> testRule = new MyCustomRule<>(MyActivity.class);
@Test
public void testStuff() {
// Wow where's all the boilerplate code?
// Verify Oscar Grouch is no longer grouchy.
}
}

view raw

MyNewTest.java

hosted with ❤ by GitHub

Where MyCustomRule handles all the test set up for the class:


public class MyCustomRule<A extends MyActivity> extends ActivityTestRule<A> {
public MyCustomRule(Class<A> activityClass) {
super(activityClass);
}
@Override
protected void beforeActivityLaunched() {
super.beforeActivityLaunched();
// Maybe prepare some mock service calls
// Maybe override some depency injection modules with mocks
}
@Override
protected Intent getActivityIntent() {
Intent customIntent = new Intent();
// add some custom extras and stuff
return customIntent;
}
@Override
protected void afterActivityLaunched() {
super.afterActivityLaunched();
// maybe you want to do something here
}
@Override
protected void afterActivityFinished() {
super.afterActivityFinshed();
// Clean up mocks
}
}

And as long as test cases use the default constructor for ActivityTestRule the test lifecycle is almost identical to before:

SideBySide

It is important to note:

  • @Before comes after Activity creation and therefore is probably not a good time to initialize all mocks.
  • The Activity will always be launched before test code begins executing.
  • It is not pictured in the above diagram but the Activity is still in onResume when @After method is executed.

ActivityTestRule: launchActivity=false;

Here is where things get interesting, ActivityTestRule’s third constructor allows developers the option to explicitly launch an activity per test case:

public ActivityTestRule(Class activityClass, boolean initialTouchMode, boolean launchActivity) {

By Passing false into the third parameter developers can easily write tests like:


@RunWith(AndroidJUnit4.class)
public class MultipleIntentsTest {
@Rule
public ActivityTestRule<MyActivity> testRule = new ActivityTestRule<>(MyActivity.class,
false, // initialTouchMode
false); // launchActivity. False to set intent per test);
@Test
public void testOscarGrouchy() {
Intent grouchyIntent = new Intent();
// intent stuff
grouchyIntent.putExtra("EXTRA_IS_GROUCHY", true);
testRule.launchActivity(grouchyIntent);
// verify Oscar is grouchy
}
@Test
public void testOscarNotGrouchy() {
Intent happyIntent = new Intent();
// intent stuff
happyIntent.putExtra("EXTRA_IS_GROUCHY", false);
testRule.launchActivity(happyIntent);
// verify Oscar is not grouchy
}
}

A powerful option for developers but does it impact the test “lifecycle”? And if so does it matter?
The answer is yes. and maybe.

The below diagram shows the difference between launchActivity=true (the default), and launchActivity=false:

launchActivityComparison

What changed?

  • @Before no longer comes after activity creation. In fact, it now comes before any other operation.
  • The activity isn’t launched until the test code makes the call to ActivityTestRule.launchActivity().

What has not changed?

  • Test and activity teardown.
  • beforeActivityLaunched(), activity creation, and afterActivityLaunched() are still in the same order.

Key Takeaways

The first takeaway is the Espresso developers are liars:

“The activity under test will be launched before each test annotated with @Test and before any method annotated with @Before. It will be terminated after the test is completed and all methods annotated with @After are finished.” –Espresso Docs

Just kidding, kind of 😛

The activity will be launched before @Test and @Before is only true for the default case, where launchActivity=true. And it is an important distinction because developers can not rely on test set up functions to prepare mocks for the activity under test. The better alternative is to prepare the activity under test in the beforeActivityLaunched() method. beforeActivityLaunched() is guaranteed to be called before activity creation regardless of launchActivity.

The second takeaway, is to be careful migrating existing tests over to the new ActivityTestRule. It can be very easy just to migrate existing onSetUp(), tearDown() methods to the very similar @Before, @After structure. That said, the right thing to do is fully embrace the new ActivityTestRule and use the exposed methods, beforeActivityLaunched and afterActivityFinished(), appropriately. BECAUSE they have guaranteed behavior in relation to the Activity lifecycle.

Finally on an unrelated note, be careful when using the new IntentsTestRule. It does not initialize, Intents.init(), until after the activity is launched (afterActivityLaunched()). Meaning if an activity tries to trigger an intent in any of its’ lifecycle methods, onCreate, onStart, or onResume, the Espresso intent framework will miss it and it will be impossible to validate or stub those intents.

Thanks for reading! Please feel free to provide feedback in the comments below or find me on twitter:

I hope you found something of value in this post.

10 thoughts on “ActivityTestRule: Espresso’s Test “Lifecycle”

  1. Hi,

    I need to revoke permissions of the app before each Espresso test.

    I have tried putting the following in both “beforeActivityLaunched” and “afterActivityFinished”:
    try {
    getInstrumentation().getUiAutomation().executeShellCommand(” pm clear “+packageName);
    }
    catch (Exception ex) {
    }
    But my tests fail because Espresso thinks the app has crashed (even though the activity should not be up during the calling of the above methods).

    Is there any way you think I could achieve this task somehow?

    Thanks in advance.

    Like

Leave a reply to sane Cancel reply