Espresso: How to take a screenshot when a test fails

11 February 2021 stoefln Leave a comment Uncategorized

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<MainActivity>()

  /**
   * 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 organise 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.

Tags: ,

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