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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
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:
Saying there is a well defined test “lifecycle” is valid because:
-
onSetUp
will always be called beforegetActivity()
getActivity()
will always come before the activity creation- Activity creation will always come before test execution
- 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,
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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. | |
} | |
} |
Where MyCustomRule handles all the test set up for the class:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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:
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, andafterActivityLaunched()
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:
Follow @jabKnowsNothing
I hope you found something of value in this post.
Thank you so much for the launchActivity tips !
LikeLiked by 1 person
Thanks!!!
LikeLiked by 1 person
Nice Post , Thanks !!
LikeLike
Great post!
LikeLike
help a lot
LikeLike
Hello Jab,
This article helps me a lot about the Android ActivityTestRule “Lifecycle”.
I want to take a note in my blog, may I refer your article in my post? Thanks!
LikeLike
go for it
LikeLiked by 1 person
Thanks a lot!
I will provide you my post url once I publish it. 🙂
LikeLike
Hello Jab,
Here’s my post about Android ActivityTestRule note (in Chinese).
https://kkboxsqa.wordpress.com/2017/09/05/android-support-test-rule/
Thanks a lot for your post, it helps me to figure out that I need to launch activity before I call it if I set launchActivity = true. 🙂
LikeLike
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.
LikeLike