【问题标题】:How to take screenshot at the point where test fail in Espresso?如何在 Espresso 测试失败时截取屏幕截图?
【发布时间】:2016-07-22 06:25:28
【问题描述】:

我正在寻找一种在测试失败后和关闭之前截取设备屏幕截图的方法。

【问题讨论】:

标签: android testing automated-tests android-espresso


【解决方案1】:

我发现的最简单的方法:

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

【讨论】:

  • 我最终得到这个错误:未能将屏幕截图保存到文件 java.io.FileNotFoundException: /storage/emulated/0/screenshots/com.myappname/asdffdsa.png (没有这样的文件或目录)
  • 通过授予写入外部存储权限来修复上述问题。现在我的问题是,当测试失败时,上面的代码不会被调用。我在手动调用中尝试了代码并且它可以工作,但我无法让它在失败时自动工作。
  • @ZeekAran 而不是使用 TestWatcher 尝试注册 espresso 失败处理程序,如此处所述 developer.android.com/training/testing/espresso/…
  • 我最终这样做了:@After public void tearDown(Scenario scenario) { if (scenario.isFailed()) {} AfterTestCleanup.tearDown(); }
【解决方案2】:

对先前答案的另一项改进。我正在使用实验性的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();
    }
  }
}

我创建了 CustomScreenCaptureProcessor 因为BasicScreenCaptureProcessor 使用 /sdcard/Pictures/ 文件夹,并且在创建文件夹/图像时我在某些设备上遇到了 IOExceptions。请注意,您需要将处理器放在同一个包中

package android.support.test.runner.screenshot;

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

然后,在您的基础 Espresso 测试类中添加

@Rule
public ScreenshotTestRule screenshotTestRule = new ScreenshotTestRule();

如果你想使用一些受保护的文件夹,这可以在模拟器上完成,但它在物理设备上不起作用

@Rule
public RuleChain screenshotRule = RuleChain
      .outerRule(GrantPermissionRule.grant(permission.WRITE_EXTERNAL_STORAGE))
      .around(new ScreenshotTestRule());

【讨论】:

  • 这确实有效,但我必须使用CustomScreenCaptureProcessor。否则我会得到这个:java.io.IOException: The directory /storage/emulated/0/Pictures/screenshots does not exist and could not be created or is not writable..
  • 你能用详细信息更新我的答案吗?我也时不时地遇到这种情况,但我没有时间去研究它。
  • 嗨@Maragues 不确定我是否理解你想要的。我刚刚使用了您提供的CustomScreenCaptureProcessor。我所做的一切都在你的回答之外。
  • 提示:屏幕截图保存在 storage/emulated/0/Android/data/{package}/files/Pictures/espresso_screenshots 中。我花了一段时间才找到他们!您可以使用 Android Studio 设备文件资源管理器一次性获取它们(右键单击文件夹 -> 另存为...)。
【解决方案3】:

@Maragues 移植到 Kotlin 的答案:

助手类:

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

用法:

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


}

在应用的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"

此设置每次在文件夹中保存一个屏幕截图:/sdcard/Android/data/your.package.name/files/Pictures 通过 Android Studio 的设备文件资源管理器(在右侧栏)导航到那里

如果您只想为失败的测试保存屏幕截图,请覆盖 TestWatcherfailed 方法而不是 finished

【讨论】:

    【解决方案4】:

    像解释的其他答案一样编写自定义 TestWatcher 是可行的方法。

    但是(我们花了很长时间才注意到它)有一个警告:规则可能触发得太晚,即在您的活动已经被破坏之后。这会为您提供设备主屏幕的屏幕截图,而不是失败活动的屏幕截图。

    你可以用RuleChain解决这个问题:而不是写

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

    你必须写:

    private final ActivityTestRule<MainActivity> _activityRule = new ActivityTestRule<>(MainActivity.class);
    
    @Rule
    public final TestRule activityAndScreenshotRule = RuleChain
            .outerRule(_activityRule)
            .around(new ScreenshotTestWatcher());
    

    这确保先截取屏幕截图,然后销毁活动

    【讨论】:

    • 遗憾的是我仍然只能获得主屏幕的图片。
    • 好吧,我让它工作了。我忽略了您需要删除活动的@Rule。我还注意到还有一个新的@get:org.junit.Rule(order = 2) 功能也可以正常工作。
    【解决方案5】:

    我对@9​​87654321@ 的回答做了一些改进。无需为 UiAutomator 添加额外的依赖项,它也可以在 api 级别 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;
       }
    }
    

    然后在您的测试类中包含以下行:

    @Rule
    public TestRule watcher = new ScreenshotTestWatcher();
    

    【讨论】:

      猜你喜欢
      • 2018-09-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-09-07
      • 1970-01-01
      • 1970-01-01
      • 2020-10-11
      • 2019-10-02
      相关资源
      最近更新 更多