Capture Android screenshots with Espresso and JUnit

By Sergei Munovarov

Original text: https://dev.to/serhuz/capturing-screenshots-on-android-with-espresso-and-junit-81f

Translation: tommwq

http://tommwq.tech/blog/%e7%94%a8espresso%e5%92%8cjunit%e6%8d%95%e6%8d%89android%e5%b1%8f%e5%b9%95%e6%88%aa%e5%9b%be/

Note: in the original android.support The package has been replaced with a new version of the Android x package.

A few days ago, I made some changes to the UI of a program. This is my spare time project. This small program is very simple, only three interfaces. And I need to upload eight screenshots to Google Play market, which will be displayed on the page of Play market. The problem is, my program supports three languages, each of which provides eight screenshots, a total of 24 screenshots. It's just for the mobile part. To show Google that my app also works with 7-inch and 10 inch tablets, I need to provide another 48 screenshots. A total of 72 screenshots. I usually take screenshots manually, but this time I'm so lazy that I decide to automate my work.

I use espresso for UI testing in my project. Package from Espresso androidx.test.runner.screenshot Have classes that capture and save screenshots on devices or emulators. In addition to taking screenshots, I also need to change the system language during testing, and only take screenshots after the interface changes.

JUnit rules can modify the behavior of test methods in a class, or perform some operations in order for the test to run smoothly. To implement JUnit rules, we need to create a class that implements the TestRule interface. TestRule contains only one method.

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 the TestRule interface and modify the system language before running the test. Here is the code:

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 to iterate over mLocales, modify the system language of the device or simulator before running the test, and restore the system language settings after the test. The problem is that this method of modifying the device system language is not officially supported, and this code cannot guarantee normal operation on all API level s. I found a wonderful piece about this article.

This code can be used in the test as follows.

@Rule
public final LocaleRule mLocaleRule = new LocaleRule(Locale.ENGLISH, Locale.FRENCH);

Each test method will be called twice with a different system language.

You can also statically import the values passed to the constructor to make the code simpler.

Because I'm going to take screenshots of three languages, I created the following Locales class.

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();
    }
}

Then instantiate the LocaleRule.

@Rule
public final LocaleRule mLocaleRule = new LocaleRule(english(), russian(), ukrainian());

I think it's very simple.

The TestWatcher class provided by JUnit can be derived as a JUnit rule to receive messages about the success or failure of a test method. I will use it as the base class to write ScreenshotWatcher.

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 in capturescreen(). Screenshot.capture() capture the visible content of the screen. This method needs to be Build.VERSION_CODES.JELLY_BEAN_MR2 and above are used, which means that some old devices cannot automatically screen. capture.setFormat ( Bitmpa.CompressFormat.PNG )Specifies the image output format, which can be JEPG, PNG or WEBP. capture.setName(name) set the screenshot file name. capture.process() method has a cryptic name that actually saves the drawing to the device storage space. Screenshots are saved to external storage by default, which is saved by androidx.test.runner.screenshot.BasicScreenCaptureProcess It's done. It's done androidx.test.runner.screenshot.ScreenCaptureProcessor Interface. The screenshot class exposes an API that allows us to use our own implementation of screensaptureprocessor.

The name of the screenshot depends on the result of the test. It can be any name you want. I use the "test method"_ Language ".

The usage of this rule is as follows:

@Rule
public final ScreenshotWatcher mScreenshotWatcher = new ScreenshotWatcher();

There's also a work to be done to get the test really started. To be able to save files to external storage, we need READ_EXTERNAL_STORAGE and WRITE_EXTERNAL_STORAGE authority. Obviously, developers need to grant permissions before executing tests.

So we need to add this to the test class:

@Rule
public final GrantPermissionRule mGrantPermissionRule = GrantPermissionRule.grant(READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE);

Now we are ready to build the ActivityRule and write some UI interaction tests. These tests produce screenshots. I use MainActivity to generate screenshots.

But for these rules to work, we need to set the order in which they are executed.

To do this, we need to change the rule declaration inside the test class.

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);

The rule chain can ensure the application order of rules. Here we need to declare mLocaleRule as outerRule, otherwise the test will not run properly.

Here is my test class.

@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 named the test MainActivityScreenshot to indicate its test purpose.

There is also a trap here. If there is no UI interaction in the test, the Activity ends immediately. Worst of all, you may only get an empty screenshot of your home screen. To deal with this, I copied the ActivityRule class into the project, adding a brief pause.

The code looks like this:

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(1000);
            } finally {
                if (instrumentation != null) {
                    instrumentation.useDefaultInterceptingActivityFactory();
                }
                if (mActivity != null) {
                    finishActivity();
                }
            }
        }
    }

The only change here is SystemClock.sleep(1000); .

Now, if we start the test on the emulator, it looks like this:

With this small amount of code, now I can automatically complete repeated operations and get screenshots in different system languages.

Moreover, without any additional changes, I can perform screenshots on different operating systems. This is a good side, because I have a Windows laptop and an Ubuntu computer. However, there are still problems in retaining language settings after configuration changes, which is a bad side.

Anyway, I hope you've learned one thing about the JUnit rules or Espresso, or two.

More information about the screenshot of Espresso can be found in here Found. JUnit has a wiki page devoted to rules in here.

Tags: Mobile Junit Android Google

Posted on Fri, 15 May 2020 03:08:12 -0700 by dsp77