Espresso: How to take a screenshot when a test fails

11 February 2021 Stephan Petzl Leave a comment Tech-Help

Ever wondered how to make debugging of tests easier by taking a screenshot right when a test fails? This is what the JUnit TestWatcher can be used for.

public class ScreenshotTestRule extends TestWatcher {

  @Override
  protected void failed(Throwable e, Description description) {
    super.failed(e, description);

    String filename = description.getTestClass().getSimpleName() + "-" + description.getMethodName();

    ScreenCapture capture = Screenshot.capture();
    capture.setName(filename);
    capture.setFormat(CompressFormat.PNG);

    HashSet processors = new HashSet<>();
    processors.add(new BasicScreenCaptureProcessor());

    try {
      capture.process(processors);
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

Configuring the storage location for the screenshot

BasicScreenCaptureProcessor uses /sdcard/Pictures/ folder and this can lead to IOExceptions on some devices when trying to save the screenshot.

You might get an error similar to this:

java.io.IOException: The directory /storage/emulated/0/Pictures/screenshots does not exist and could not be created or is not writable.

That’s why we override the basic functionality and configure another directory:

package android.support.test.runner.screenshot;

public class CustomScreenCaptureProcessor extends BasicScreenCaptureProcessor {    
  public CustomScreenCaptureProcessor() {
    super(
        new File(
            InstrumentationRegistry.getTargetContext().getExternalFilesDir(DIRECTORY_PICTURES), "test_run_screenshots"
        )
    );
  }
}

Be aware that CustomScreenCaptureProcessor.java needs to be placed in the same directory as your ScreenshotTestRule.java. Of cause, you need to make use of it inside your TestWatcher class:

Instead of

processors.add(new BasicScreenCaptureProcessor());

use

processors.add(new CustomScreenCaptureProcessor());

Timing of taking the screenshot

If you use the rule, as I outlined above, it might take the screenshot too late. For example, your activity might crash and all you get is a screenshot of the home screen.

There is a RuleChain that allows you to get the timing right. This is the code without the RuleChain:

@Rule
public final ActivityTestRule _activityRule = new ActivityTestRule<>(MainActivity.class);

@Rule
public ScreenshotTestWatcher _screenshotWatcher = new ScreenshotTestWatcher();

And this is the code with a RuleChain, to make sure that the screenshot is taken BEFORE the activity is destroyed:

private final ActivityTestRule _activityRule = new ActivityTestRule<>(MainActivity.class);

@Rule
public final TestRule activityAndScreenshotRule = RuleChain
        .outerRule(_activityRule)
        .around(new ScreenshotTestWatcher());

How to take a screenshot of a view:

There is a relatively new API for taking screenshots of either the whole screen or individual screenshots by using the AndroidX screenshot API. Here is an example of how it works:

package com.myapp.screenshotTesting

import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestName
import org.junit.runner.RunWith
import java.io.IOException

import androidx.test.core.app.takeScreenshot
import androidx.test.core.graphics.writeToTestStorage
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.espresso.screenshot.captureToBitmap
import androidx.test.ext.junit.rules.activityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4

/*
* This example shows how we can take screenshots of portions of the screen and save them to test storage.
* If this test is executed via gradle managed devices, the saved image files will be stored at
* build/outputs/managed_device_android_test_additional_output/debugAndroidTest/managedDevice/[DeviceModel]Api[ApiVersion]/
*/
@RunWith(AndroidJUnit4::class)
class ScreenshotExampleTest {

  // we can use a JUnit rule that stores the method name inside this class property, so it can be used to generate a unique filename for each screenshot taken 
  @get:Rule
  var nameRule = TestName()

  @get:Rule
  val activityScenarioRule = activityScenarioRule()

  /**
   * Captures and saves an image of root view, so basically all the app UI.
   */
  @Test
  @Throws(IOException::class)
  fun saveActivityBitmap() {
    onView(isRoot())
      .captureToBitmap()
      .writeToTestStorage("${javaClass.simpleName}_${nameRule.methodName}")
  }

  /**
   * Captures and saves an image of the Login Button.
   */
  @Test
  @Throws(IOException::class)
  fun saveViewBitmap() {
    onView(withText("Login"))
      .captureToBitmap()
      .writeToTestStorage("${javaClass.simpleName}_${nameRule.methodName}")
  }

  /**
   * Captures and saves an image of the entire device screen to storage.
   */
  @Test
  @Throws(IOException::class)
  fun saveDeviceScreenBitmap() {
    takeScreenshot()
      .writeToTestStorage("${javaClass.simpleName}_${nameRule.methodName}")
  }
}

Moving the screenshots to the test automation host:

Probably you want to send the screenshots via email, slack or make them available via a webserver. To do that you can use Android Debug Bridge (ADB):

Simply call adb pull to copy the files over to your host system:

adb pull "/storage/sdcard0/DCIM/test_run_screenshots"

 

How to organize test screenshots

Taking screenshots on many different devices can become messy. Spoon can help to stay organised:
Spoon makes existing instrumentation tests more useful by running tests on multiple devices simultaneously. An HTML summary with detailed information about each device and test including screenshots is generated.

Learn more about testing with Espresso

Like this article? there’s more where that came from!