Capturing Screenshots on Android with Espresso and JUnit
Tags: android, espresso, automation, testing
Published: 2017-12-10 23:57:30

---
title: Capturing Screenshots on Android with Espresso and JUnit
published: true
description: 
tags: #android #espresso #automation #testing
---

The other day I did some changes to the UI of an app, on which I work in my free time. This app is quite small and simple, there are only 3 screens, and I need to upload 8 screenshots to Google Play which will be shown on app's page. Problem is, the app has 3 locales, so I'd have to provide 8 screenshots for each of them. This makes a total of 24 screenshots. And that's only for smartphones. There are also tablets with 7" and 10" screens, and in order to convince Google Play that the app in question is optimized for such devices as well, I'd have to provide another 48 screenshots. 72 screenshots in total. Usually I'd do this manually, but I was feeling lazy all of a sudden. So I decided to automate this process. 

I already use `Espresso` for UI testing in my project and it provides `android.support.test.runner.screenshot` package which contains classes, required to capture and save scrrenshots on a test device or an emulator. Apart from capturing actual screenshots it is necessary to change locale somehow while running tests and capture screenshots only after UI changes. 

***
JUnit rules allow to modify behavior of each test method in a class. Or they can do some work in order to make tests work at all. In order to implement such a rule we need to create a class that implements `TestRule` interface which contains only one method

```java
public interface TestRule {
    /**
     * Modifies the method-running {@link Statement} to implement this
     * test-running rule.
     *
     * @param base The {@link Statement} to be modified
     * @param description A {@link Description} of the test implemented in {@code base}
     * @return a new statement, which may be the same as {@code base},
     *         a wrapper around {@code base}, or a completely new Statement.
     */
    Statement apply(Statement base, Description description);
}
```

Let's implement a `TestRule` which will change locale before running a test. Here's the code:
```java
public class LocaleRule implements TestRule {

    private final Locale[] mLocales;
    private Locale mDeviceLocale;


    public LocaleRule(Locale... locales) {
        mLocales = locales;
    }


    @Override
    public Statement apply(Statement base, Description description) {
        return new Statement() {
            @Override
            public void evaluate() throws Throwable {
                try {
                    if (mLocales != null) {
                        mDeviceLocale = Locale.getDefault();
                        for (Locale locale : mLocales) {
                            setLocale(locale);
                            base.evaluate();
                        }
                    }
                } finally {
                    if (mDeviceLocale != null) {
                        setLocale(mDeviceLocale);
                    }
                }
            }
        };
    }


    private void setLocale(Locale locale) {
        Resources resources = InstrumentationRegistry.getTargetContext().getResources();
        Locale.setDefault(locale);
        Configuration config = resources.getConfiguration();
        config.setLocale(locale);
        DisplayMetrics displayMetrics = resources.getDisplayMetrics();
        resources.updateConfiguration(config, displayMetrics);
    }
}
``` 
Basically, what this rule does is iterates over `mLocales` and changes locale on the device/emulator before running the test. After that locale is changed back. Main problem of this approach is that changing device locale like this isn't officially supported and this code may not work on all API levels. I've found a nice article about this [here](https://proandroiddev.com/change-language-programmatically-at-runtime-on-android-5e6bc15c758). Otherwise, it's really simple.

It can be used in a test the following way:
```java
@Rule
public final LocaleRule mLocaleRule = new LocaleRule(Locale.ENGLISH, Locale.FRENCH);
```
In this case _each_ test method will be invoked 2 times with different locales.

You can also static import values passed into constructor to make the code less verbose.

Since I'm doing screenshots for 3 locales, I've created `Locales` class.
```java
public final class Locales {

    private Locales() {
        throw new AssertionError();
    }


    public static Locale english() {
        return Locale.ENGLISH;
    }


    public static Locale russian() {
        return new Locale.Builder().setLanguage("ru").build();
    }


    public static Locale ukrainian() {
        return new Locale.Builder().setLanguage("uk").build();
    }
}
```
And then I instantiate `LocaleRule` like this:
```java
@Rule
public final LocaleRule mLocaleRule = new LocaleRule(english(), russian(), ukrainian());
```
Looks tidy to me.

***
JUnit also offers `TestWatcher` class that can be extended and used as a `@Rule` to receive notifications when test methods succeed of fail. I'll be using this class as a base to implement my `ScreenshotWatcher`.

```java
public class ScreenshotWatcher extends TestWatcher {

    @Override
    protected void succeeded(Description description) {
        Locale locale = InstrumentationRegistry.getTargetContext()
                .getResources()
                .getConfiguration()
                .getLocales()
                .get(0);
        captureScreenshot(description.getMethodName() + "_" + locale.toLanguageTag());
    }


    private void captureScreenshot(String name) {
        ScreenCapture capture = Screenshot.capture();
        capture.setFormat(Bitmap.CompressFormat.PNG);
        capture.setName(name);
        try {
            capture.process();
        } catch (IOException ex) {
            throw new IllegalStateException(ex);
        }
    }


    @Override
    protected void failed(Throwable e, Description description) {
        captureScreenshot(description.getMethodName() + "_fail");
    }
}
```
Let's look at the code inside `captureScreenshot()` method closer. `Screenshot.capture();` captures visible screen content **for Build.VERSION_CODES.JELLY_BEAN_MR2 and above** which means no automated screenshooting on older devices. `capture.setFormat(Bitmap.CompressFormat.PNG);` specifies output format for the given screenshot. Can be `JPEG`, `PNG` or `WEBP`. `capture.setName(name);` sets name for the screenshot file. `capture.process();` actually saves the image to the device storage despite its somewhat obscured name. By default screenshots are saved to external storage. This is handled by `android.support.test.runner.screenshot.BasicScreenCaptureProcessor` which implements `android.support.test.runner.screenshot.ScreenCaptureProcessor` interface. `Screenshot` class exposes an API that allows to use custom `ScreenCaptureProcessor` implementations.

`name` parameter passed to this method depends on the result of each test. It can be whatever you decide it to be. I went with `testMethodName_currentLocale` when test is successful. 

This `@Rule` is then used like this:
```java
@Rule
public final ScreenshotWatcher mScreenshotWatcher = new ScreenshotWatcher();
```

But there's one more thing that is required to make this actually work. To be able to save files to external storage we need *READ_EXTERNAL_STORAGE* and *WRITE_EXTERNAL_STORAGE* permissions. And, somewhat unsurprisingly, it's up to developer to grant them before running the test.

So we just have to add this to the test class:
```java
@Rule
public final GrantPermissionRule mGrantPermissionRule = 
        GrantPermissionRule.grant(READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE);
```

***
After this we are actually ready to create an `ActivityRule` and write some tests with UI interactions which will trigger our screenshots. I'll be using my `MainActivity`.

But in order to make all those rules work predictably we need to enforce an order in which they'll be applied.

In order to do this, we need to change rule declaration inside the test class.
```java
private final LocaleRule mLocaleRule = new LocaleRule(english(), russian(), ukrainian());
private final ScreenshotWatcher mScreenshotWatcher = new ScreenshotWatcher();
private final GrantPermissionRule mGrantPermissionRule = 
        GrantPermissionRule.grant(READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE);
private final ActivityTestRule mActivityTestRule = new ActivityTestRule<>(MainActivity.class);

@Rule
public final RuleChain mRuleChain = RuleChain.outerRule(mLocaleRule)
        .around(mScreenshotWatcher)
        .around(mGrantPermissionRule)
        .around(mActivityTestRule);
```
And `RuleChain` does just that. Here we must declare `mLocaleRule` as outerRule otherwise the expected behavior is broken.

Here's how my test class looks like
```java
@RunWith(AndroidJUnit4.class)
public class MainActivityScreenshot {

    private final LocaleRule mLocaleRule = new LocaleRule(english(), russian(), ukrainian());
    private final ScreenshotWatcher mScreenshotWatcher = new ScreenshotWatcher();
    private final GrantPermissionRule mGrantPermissionRule = GrantPermissionRule.grant(READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE);
    private final ActivityTestRule mActivityTestRule = new ActivityTestRule<>(MainActivity.class);

    @Rule
    public final RuleChain mRuleChain = RuleChain.outerRule(mLocaleRule)
            .around(mScreenshotWatcher)
            .around(mGrantPermissionRule)
            .around(mActivityTestRule);


    @Test
    public void emptyMainActivityPortrait() {
    }
}
```

I've named this test class `MainActivityScreenshot` to indicate its purpose.

But there's yet another pitfall. If there are no UI interactions in the test Activity is finished almost instantly and in worst-case scenario you'll just end up with empty home screen on a screenshot. To cater for this, I've copied `ActivityRule` class to my project and added a small pause after the test.

It looks like this:
```java
private class ActivityStatement extends Statement {

        private final Statement mBase;


        public ActivityStatement(Statement base) {
            mBase = base;
        }


        @Override
        public void evaluate() throws Throwable {
            MonitoringInstrumentation instrumentation =
                    CustomActivityTestRule.this.mInstrumentation instanceof MonitoringInstrumentation
                            ? (MonitoringInstrumentation) CustomActivityTestRule.this.mInstrumentation
                            : null;
            try {
                if (mActivityFactory != null && instrumentation != null) {
                    instrumentation.interceptActivityUsing(mActivityFactory);
                }
                if (mLaunchActivity) {
                    launchActivity(getActivityIntent());
                }
                mBase.evaluate();

                SystemClock.sleep(1_000);
            } finally {
                if (instrumentation != null) {
                    instrumentation.useDefaultInterceptingActivityFactory();
                }
                if (mActivity != null) {
                    finishActivity();
                }
            }
        }
    }
```
The only change is `SystemClock.sleep(1_000);`.

***
Now, if we launch the test on the emulator, it looks like this:
![](https://thepracticaldev.s3.amazonaws.com/i/tq4habzzopjdkw99flsw.gif)

***
So with a bit of a code, I'm now able to automate the process of repeating same actions with different locales and taking screenshots. 

Once implemented, I don't have to do any additional setup to make this work on different OSes, which is good, because I have a Windows laptop and Ubuntu-based desktop. But there's also a problem with retaining locale settings on configuration change, which is not so good.

Anyway, hopefully you've learned a thing or two about JUnit Rules and Espresso.

More information about Espresso screenshot package can be found [here](https://developer.android.com/reference/android/support/test/runner/screenshot/package-summary.html). JUnit has a wiki page dedicated to Rules [here](https://github.com/junit-team/junit4/wiki/Rules).