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



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:



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. Otherwise, it's really simple.

It can be used in a test the following way:



@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.



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:



@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 .



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:



@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:



@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.



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



@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:



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:



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. JUnit has a wiki page dedicated to Rules here.