22

I am looking for a way to take a screenshot of device after test failed and before get closed.

think_better
  • 343
  • 1
  • 2
  • 11

6 Answers6

20

Easiest way that I found:

@Rule
public TestRule watcher = new TestWatcher() {
  @Override
  protected void failed(Throwable e, Description description) {
    // Save to external storage (usually /sdcard/screenshots)
    File path = new File(Environment.getExternalStorageDirectory().getAbsolutePath()
        + "/screenshots/" + getTargetContext().getPackageName());
    if (!path.exists()) {
      path.mkdirs();
    }

    // Take advantage of UiAutomator screenshot method
    UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
    String filename = description.getClassName() + "-" + description.getMethodName() + ".png";
    device.takeScreenshot(new File(path, filename));
  }
};
Kevin Brotcke
  • 3,765
  • 26
  • 34
  • 1
    I end up with this error: failed to save screen shot to file java.io.FileNotFoundException: /storage/emulated/0/screenshots/com.myappname/asdffdsa.png (No such file or directory) – Zeek Aran Apr 26 '18 at 16:02
  • 1
    Fixed the above by granting write external storage permissions. Now my issue is that the above code doesn't get called when a test fails. I tried the code in a manual call and it worked, but I can't get it to work upon failure automatically. – Zeek Aran Apr 26 '18 at 20:04
  • @ZeekAran instead of using a `TestWatcher` try registering espresso failure handler as described here https://developer.android.com/training/testing/espresso/recipes#using-custom-failure-handler – Denys Kniazhev-Support Ukraine Apr 30 '18 at 09:17
  • I ended up doing it this way: @After public void tearDown(Scenario scenario) { if (scenario.isFailed()) {} AfterTestCleanup.tearDown(); } – Zeek Aran May 01 '18 at 13:06
17

Another improvement to previous answers. I'm using the experimental Screenshot API

public class ScreenshotTestRule extends TestWatcher {

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

    takeScreenshot(description);
  }

  private void takeScreenshot(Description description) {
    String filename = description.getTestClass().getSimpleName() + "-" + description.getMethodName();

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

    HashSet<ScreenCaptureProcessor> processors = new HashSet<>();
    processors.add(new CustomScreenCaptureProcessor());

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

I've created CustomScreenCaptureProcessor because BasicScreenCaptureProcessor uses /sdcard/Pictures/ folder and I encountered IOExceptions on some devices when creating the folder/image. Please note that you need to place your processor in the same package

package android.support.test.runner.screenshot;

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

Then, in your base Espresso test class just add

@Rule
public ScreenshotTestRule screenshotTestRule = new ScreenshotTestRule();

If you wish to use some protected folder, this did the trick on an emulator, tho it didn't work on a physical device

@Rule
public RuleChain screenshotRule = RuleChain
      .outerRule(GrantPermissionRule.grant(permission.WRITE_EXTERNAL_STORAGE))
      .around(new ScreenshotTestRule());
Maragues
  • 37,861
  • 14
  • 95
  • 96
  • 1
    This did work, but I have to use the `CustomScreenCaptureProcessor`. Otherwise I get this: `java.io.IOException: The directory /storage/emulated/0/Pictures/screenshots does not exist and could not be created or is not writable.`. – Albert Vila Calvo Nov 29 '18 at 11:48
  • Could you update my answer with the details? I'm also experiencing that every now and then, but I haven't had the time to look into it. – Maragues Nov 29 '18 at 12:04
  • Hi @Maragues not sure if I understand what you want. I just used the `CustomScreenCaptureProcessor` you provided. There's nothing I did that it's not on your answer. – Albert Vila Calvo Nov 29 '18 at 12:22
  • Hint: screenshots are saved in _storage/emulated/0/Android/data/{package}/files/Pictures/espresso_screenshots_. It took me a while find them! You can grab them all at once with the Android Studio Device File Explorer (right click on the folder -> Save As...). – Albert Vila Calvo Nov 30 '18 at 10:56
  • More details at https://medium.com/stepstone-tech/how-to-capture-screenshots-for-failed-ui-tests-9927eea6e1e4 – garnet May 17 '19 at 06:47
  • @AlbertVilaCalvo if you use a Google System image on the emulator the _storage/emulated/0/Android/data/{package}/files/Pictures/espresso_screenshots_ can be found in _/sdcard/Android/data/{package}/files/Pictures/espresso_screenshots_ since it's a symlink. – Bruno Bieri Oct 22 '20 at 15:52
7

@Maragues answer ported to Kotlin:

Helper classes:

package utils

import android.graphics.Bitmap
import android.os.Environment.DIRECTORY_PICTURES
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import androidx.test.runner.screenshot.BasicScreenCaptureProcessor
import androidx.test.runner.screenshot.ScreenCaptureProcessor
import androidx.test.runner.screenshot.Screenshot
import org.junit.rules.TestWatcher
import org.junit.runner.Description
import java.io.File
import java.io.IOException

class IDTScreenCaptureProcessor : BasicScreenCaptureProcessor() {
    init {
        mTag = "IDTScreenCaptureProcessor"
        mFileNameDelimiter = "-"
        mDefaultFilenamePrefix = "Giorgos"
        mDefaultScreenshotPath = getNewFilename()
    }

    private fun getNewFilename(): File? {
        val context = getInstrumentation().getTargetContext().getApplicationContext()
        return context.getExternalFilesDir(DIRECTORY_PICTURES)
    }
}

class ScreenshotTestRule : TestWatcher() {
    override fun finished(description: Description?) {
        super.finished(description)

        val className = description?.testClass?.simpleName ?: "NullClassname"
        val methodName = description?.methodName ?: "NullMethodName"
        val filename = "$className - $methodName"

        val capture = Screenshot.capture()
        capture.name = filename
        capture.format = Bitmap.CompressFormat.PNG

        val processors = HashSet<ScreenCaptureProcessor>()
        processors.add(IDTScreenCaptureProcessor())

        try {
            capture.process(processors)
        } catch (ioException: IOException) {
            ioException.printStackTrace()
        }
    }
}

Usage:

import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.rule.ActivityTestRule
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import utils.ScreenshotTestRule

@RunWith(AndroidJUnit4::class)
@LargeTest
class DialogActivityTest {

    @get:Rule
    val activityRule = ActivityTestRule(DialogActivity::class.java)

    @get:Rule
    val screenshotTestRule = ScreenshotTestRule()

    @Test
    fun dialogLaunch_withTitleAndBody_displaysDialog() {
        // setup
        val title = "title"
        val body = "body"

        // assert
        onView(withText(title)).check(matches(isCompletelyDisplayed()))
        onView(withText(body)).check(matches(isCompletelyDisplayed()))
    }


}

Libraries declared in app's build.gradle:

androidTestImplementation "androidx.test.espresso:espresso-core:3.1.1"
androidTestImplementation "androidx.test.espresso:espresso-intents:3.1.1"
androidTestImplementation "androidx.test.ext:junit:1.1.0"
androidTestImplementation "androidx.test:runner:1.1.1"
androidTestImplementation "androidx.test:rules:1.1.1"

This setup saves a screenshot every time a test finished in the folder: /sdcard/Android/data/your.package.name/files/Pictures Navigate there via Android Studio's Device File Explorer (on the right sidebar)

If you like to save screenshots only for failed tests, override the failed method of TestWatcher instead of the finished

giorgos.nl
  • 2,704
  • 27
  • 36
4

Writing a custom TestWatcher like the other answers explained is the way to go.

BUT (and it took us a long time to notice it) there is a caveat: The rule might fire too late, i.e. after your activity was already destroyed. This leaves you with a screenshot of the device's home screen and not from the failing activity.

You can solve this using RuleChain: Instead of writing

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

@Rule
public ScreenshotTestWatcher _screenshotWatcher = new ScreenshotTestWatcher();

You have to write:

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

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

This makes sure that the screenshot is taken first and then the activity is destroyed

PhilLab
  • 4,777
  • 1
  • 25
  • 77
  • Sadly I still only get pictures of the home screen. – Martin Jul 21 '21 at 14:01
  • 1
    Well, I got it to work. I overlooked that you need to remove the `@Rule` for the activity. I also noted that there is also a new `@get:org.junit.Rule(order = 2)` feature which works as well. – Martin Jul 29 '21 at 14:00
0

I made some improvements on this answer. No need to add an extra dependency for UiAutomator and it also works below api level 18.

public class ScreenshotTestWatcher extends TestWatcher
{
   private static Activity currentActivity;

   @Override
   protected void failed(Throwable e, Description description)
   {
      Bitmap bitmap;

      if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2)
      {
         bitmap = getInstrumentation().getUiAutomation().takeScreenshot();
      }
      else
      {
         // only in-app view-elements are visible.
         bitmap = Screenshot.capture(getCurrentActivity()).getBitmap();
      }

      // Save to external storage '/storage/emulated/0/Android/data/[package name app]/cache/screenshots/'.
      File folder = new File(getTargetContext().getExternalCacheDir().getAbsolutePath() + "/screenshots/");
      if (!folder.exists())
      {
         folder.mkdirs();
      }

      storeBitmap(bitmap, folder.getPath() + "/" + getFileName(description));
   }

   private String getFileName(Description description)
   {
      String className = description.getClassName();
      String methodName = description.getMethodName();
      String dateTime = Calendar.getInstance().getTime().toString();

      return className + "-" + methodName + "-" + dateTime + ".png";
   }

   private void storeBitmap(Bitmap bitmap, String path)
   {
      BufferedOutputStream out = null;
      try
      {
         out = new BufferedOutputStream(new FileOutputStream(path));
         bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
      }
      catch (IOException e)
      {
         e.printStackTrace();
      }
      finally
      {
         if (out != null)
         {
            try
            {
               out.close();
            }
            catch (IOException e)
            {
               e.printStackTrace();
            }
         }
      }
   }

   private static Activity getCurrentActivity()
   {
      getInstrumentation().runOnMainSync(new Runnable()
         {
            public void run()
            {
               Collection resumedActivities = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(
                     RESUMED);
               if (resumedActivities.iterator().hasNext())
               {
                  currentActivity = (Activity) resumedActivities.iterator().next();
               }
            }
         });

      return currentActivity;
   }
}

Then include the following line in your test class:

@Rule
public TestRule watcher = new ScreenshotTestWatcher();
Wirling
  • 4,810
  • 3
  • 48
  • 78
0

A compilation of the other suggestions, comments and some of my own. I was having issues with the screenshot not being taken within the TestWatcher failed function. Instead using a custom FailureHandler, since it was only mentioned and no example provided, here goes...

class ScreenshotRule : TestWatcher() {

    override fun starting(description: Description) {
        super.starting(description)
        setFailureHandler(
            CustomFailureHandler(getInstrumentation().targetContext, description)
        )
    }
}

class CustomFailureHandler(targetContext: Context, description: Description) : FailureHandler {

    private val delegate: FailureHandler
    private val device = UiDevice.getInstance(getInstrumentation())
    private val directory = "/sdcard/Pictures/screenshots/failures"
    private val testClassName = description.className
    private val testMethodName = description.methodName

    init {
        delegate = DefaultFailureHandler(targetContext)
    }

    override fun handle(error: Throwable, viewMatcher: Matcher<View>) {
        val snapshot = File("$directory/$testClassName", "$testMethodName.png")
        snapshot.parentFile!!.mkdirs()
        device.takeScreenshot(snapshot)

        delegate.handle(error, viewMatcher)
    }
}

I have this Rule ordered after my 'GrantPermissionRule' and 'activityScenarioRule'

Also couldn't trust the Environment dirs Environment.getExternalStorageDirectory() or targetContext.getExternalFilesDir(Environment.DIRECTORY_SCREENSHOTS) so just providing my own to the sdcard/Pictures/screenshots. Maybe someone can help improve the use of MediaStore...