Cutting boilerplate in UI tests
If you have experience writing UI tests for android, you probably heard of tools like Spoon or Composer. Along with orchestration of test execution, they provide APIs for capturing screenshots which later are placed into generated HTML report. All of these is great, but there are certain associated shortcomings:
- Screenshot capturing logic clutters tests
- Hard to replace one library by another
Here is the example of an instrumentation test for a simple form screen:
@RunWith(AndroidJUnit4.class)
public class FormScreenTest {
@Rule
public ActivityTestRule<FormActivity> activityTestRule =
new ActivityTestRule<>(FormActivity.class);
@Test
public void clickOnSubmitMustHighlightErrors() {
Spoon.screenshot(testRule.getActivity(), "state_before_edit");
onView(withId(R.id.title)).perform(replaceText("test_title"));
onView(withId(R.id.description)).perform(replaceText("test_description"));
onView(withId(R.id.submit)).perform(click());
Spoon.screenshot(testRule.getActivity(), "state_after_edit");
}
}
Here I am using Spoon
to capture screenshots which represent the state before and after user actions. This looks fine, but screenshot capturing logic shouldn’t really be a part of the test body. Moreover, it simply becomes tedious to enter tags by hand, when they can be automatically derived from the test name. This is what we can have instead:
@RunWith(AndroidJUnit4.class)
public class FormScreenTest {
@Rule
public ScreenshotsRule<FormActivity> screenshotsRule =
new ScreenshotsRule<>(FormActivity.class);
@Test
@CaptureScreenshots
public void clickOnSubmitMustHighlightErrors() {
onView(withId(R.id.title)).perform(replaceText("test_title"));
onView(withId(R.id.description)).perform(replaceText("test_description"));
onView(withId(R.id.submit)).perform(click());
}
}
Looks better huh? Here is another example where tags are specified explicitly and the state of activity is captured within test using the screenshot("tag")
method:
@RunWith(AndroidJUnit4.class)
public class FormScreenTest {
@Rule
public ScreenshotsRule<FormActivity> screenshotsRule =
new ScreenshotsRule<>(FormActivity.class);
@Test
@CaptureScreenshots(before = "before_state", after = "after_state")
public void clickOnSubmitMustHighlightErrors() {
onView(withId(R.id.title)).perform(replaceText("test_title"));
onView(withId(R.id.description)).perform(replaceText("test_description"));
// capture screenshot within test
screenshotsRule.screenshot("intermediate_state");
onView(withId(R.id.submit)).perform(click());
}
}
Implementing ScreenshotsRule
So how do we get there? By writing a jUnit rule which knows where to grab an instance of Activity
and when to invoke Spoon
. First of all, let’s create an annotation for marking tests which we want to “screenshot”:
@Target(METHOD)
@Retention(RUNTIME)
public @interface CaptureScreenshots {
String before() default "";
String after() default "";
}
Since we have to follow ActivityTestRule
’s lifecycle, we will have to build our rule upon it:
public class ScreenshotsRule<T extends Activity> extends ActivityTestRule<T> {
private Description description;
public ScreenshotsRule(Class<T> clazz) {
super(clazz);
}
@Override
public Statement apply(Statement base, Description description) {
this.description = description;
return super.apply(base, description);
}
@Override
public void finishActivity() {
if (getActivity() != null) {
beforeActivityFinished();
}
super.finishActivity();
}
public void screenshot(String tag) {
if (getActivity() == null) {
throw new IllegalStateException("Activity has not been started yet" +
" or has already been killed!");
}
Spoon.screenshot(getActivity(), tag, description.getClassName(),
description.getMethodName());
}
Override
protected void afterActivityLaunched() {
CaptureScreenshots captureScreenshots =
description.getAnnotation(CaptureScreenshots.class);
if (captureScreenshots != null) {
String tag = captureScreenshots.before();
if (TextUtils.isEmpty(tag)) {
tag = "before_" + description.getMethodName();
}
screenshot(tag);
}
}
protected void beforeActivityFinished() {
CaptureScreenshots captureScreenshots =
description.getAnnotation(CaptureScreenshots.class);
if (captureScreenshots != null) {
String tag = captureScreenshots.after();
if (TextUtils.isEmpty(tag)) {
tag = "after_" + description.getMethodName();
}
screenshot(tag);
}
}
}
I had to create a synthetic
beforeActivityFinished()
callback by overridingfinishActivity()
method, which is not the “cleanest” solution. See the feature request for adding it toActivityTestRule
in support library.
For the sake of simplicity and brevity of example, I have not overridden all of the parent constructors, but it is something what you most likely end-up doing in real world scenarios.
Within the apply
method we are storing a reference to a Description
object that contains useful metadata about the test. When a @CaptureScreenshots
annotation is present, we have to take screenshots after activity has been launched and before it will be finished. When tags have not been supplied to the annotation, the name of the test method will be used as a basis for generating them.
Now we can substitute this rule for ActivityTestRule
and we are good to go.
Wrapping up
We have less boilerplate to write in tests! Moreover, dependency on Spoon or any other plugin you might be using is abstracted away. It means switching to another solution in the future will be less painful.
Thanks to Mark Polak for proofreading this article.
Update
Here is the link to the GitHub gist with the ScreenshotsRule implementation.