是否可以截屏并用浓缩咖啡测试像素值?

Posted

技术标签:

【中文标题】是否可以截屏并用浓缩咖啡测试像素值?【英文标题】:Is it possible to take a screenshot and test pixel values with espresso? 【发布时间】:2013-11-12 11:53:45 【问题描述】:

我开始使用 espresso 并编写了一些简单的 UI 测试,用于单击按钮、输入文本并检查所有内容是否都在应有的位置。 现在我想截屏并测试一些像素值,但我找不到这样做的方法。 有人有什么建议吗?浓缩咖啡也可以吗?

【问题讨论】:

【参考方案1】:

由于 espresso 测试是简单的 InstrumentationTestCases,您可以使用 paparazzo and celebrity 为您的应用程序截取屏幕截图。

Spoon 也是一个非常不错的截图系统。比狗仔队更清晰,但只能截取活动截图,而不是对话。

最后但同样重要的是,从 android SDK 16 开始,您可以使用 screencap utility 对应用进行非常快速和清晰的屏幕截图。我认为这个解决方案确实是最好的,但它只适用于 SDK 16+。

最后要对照参考截图测试截图,没有真正的工具。你可以在 stack over flow 上找到很棒的实现想法,但到目前为止,还没有一个可以在 android 上使用的参考工具。

【讨论】:

感谢您的回复。问题是,我实际上需要测试 LiveWallpaperService。我需要截取 LiveWallpaper 的 PreviewMode 和主屏幕的截图,然后对它们进行像素测试,以便我可以查看它们是否相同。 在我看来,实现这种方式相当困难。没有办法获取用于墙纸的文件名吗? 我不能这样做,因为用户可以选择通过更改不同的图片和行为来制作自己的动态壁纸,而且我无法从显示整个动态壁纸的位置读取文件。我会尝试为此找到一些解决方法 因此,您应该真正标记几个像素以轻松检测它们。您可以使用一些隐写术技术:webrevud.com/…【参考方案2】:

如果您正在寻找,Facebook 会提供一个工具来比较屏幕截图:http://facebook.github.io/screenshot-tests-for-android/

迭代 UI 代码很困难。您如何快速验证您的布局或视图更改在所有配置中都能正常工作? screenshot-tests-for-android 可以通过提供一个测试框架来检查更改之间的视觉差异,从而解决这些问题。

【讨论】:

【参考方案3】:

我创建了一个版本,以将 Espresso 与 Spoon 一起运行添加到适用于 android 的优质工具中,check it out

只要运行你就会看到:

mvn clean install -P espresso-spoon

【讨论】:

什么的分叉版本?回购中的最后一次更新是 4 年多以前.. 有没有计划在过去几年更新/维护反射测试工具的改进?谢谢【参考方案4】:

作为进行屏幕截图比较的替代方法,您可以考虑使用诸如 LayoutVerifier 之类的库来比较布局状态本身(即视图的位置)。

虽然它不会以屏幕截图的形式为您提供布局的可视化表示,但它本质上通过比较视图的状态来执行相同的功能。它还可以在 Robolectric 上运行,无需在真实设备上运行。

【讨论】:

【参考方案5】:

为my custom view 设置屏幕截图测试我付出了很多努力。 以下是我如何做到这一点以及我在此过程中学到的一切。 它也可以被常规应用程序和活动使用。

⚠ 注意#1

如果需要,您可以使用 JUnit 4。我正在使用 JUnit 5。因为 JUnit 5 从头开始​​构建在 Java 8 之上,所以它的插桩测试只能在运行 Android 8.0 (API 26) 或更新版本的设备上运行。较旧的手机或模拟器将完全跳过这些测试的执行,将它们标记为忽略

如果您想在 Android 上运行 JUnit 5 测试,请参阅 this answer 了解如何设置。

⚠ 注意#2

屏幕截图测试可能无法在其他设备上运行,即使它们具有相同的屏幕 DPI(它们可能根本无法在具有不同屏幕 DPI 的设备上运行)。例如,即使我在本地机器和 GitHub Actions 上使用相同的设备运行测试,它们也不会产生相同的结果(GitHub Actions 断言失败)。所以,我不得不在 GitHub Actions 上禁用它们。

如果您想在 GitHub Actions 上禁用屏幕截图测试,请参阅 this answer。

⚠ 注意#3

如果您在检测测试中有资源(在 androidTest 目录的子目录中)并且您想引用它们的 id,您应该像这样使用它们:

my.package.name.test.R.id.an_id

例如,如果您的包名称是 com.example,那么要在您的测试中访问 src/androidTest/res/layout/my_layout.xml 中的布局文件,您可以使用 com.example.test.R.layout.my_layout

⚠ 注意#4

由于我们将测试屏幕截图保存在设备/模拟器的外部存储中,因此我们需要确保在清单和 adb 安装选项中添加了 WRITE_EXTERNAL_STORAGE 权限- g-r 在构建脚本中配置。在 Marshmallow+ 上运行时,我们还需要在运行测试之前授予这些权限。 -g 用于在安装应用程序时授予权限(仅适用于 Marshmallow+),而 -r 用于允许重新安装应用程序。 这些对应于adb shell pm install 选项。 请注意,这不适用于 Android Studio。

src/androidTest/目录下创建一个AndroidManifest.xml文件,并在其中添加以下内容:

<manifest package="com.example">
  <!-- For saving screenshots in tests -->
  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
                   tools:ignore="ScopedStorage"
                   tools:remove="android:maxSdkVersion"/>
  <application android:requestLegacyExternalStorage="true">
   <!-- In case your test activity is in *androidTest* source set
        (I have it like this to test my custom view), define it here -->
    <activity android:name=".MyActivityThatContainsTheView"/>
  </application>
</manifest>

并在应用构建文件中添加 adb 安装选项:

android 
    // adbOptions block is deprecated in newer version of Android Gradle Plugin;
    // replace adbOptions block with installation block
    adbOptions 
        installOptions("-g", "-r")
    


⚠ 注意#5

我将参考屏幕截图(我想与当前屏幕截图比较的那个)保存在 src/androidTest/assets 目录中。因此,将该目录指定为应用构建脚本中的 assets 条目:

android 
    sourceSets 
        get("debug").assets.srcDirs("src/androidTest/assets")
    

⚠ 注意#6

在运行测试时传递检测参数(如以下代码中的shouldSaveshouldAssert):

分级任务: 从命令行运行任务:在任务名称后传递参数./gradlew myTask -Pandroid.testInstrumentationRunnerArguments.shouldSave=true 使用 Android Studio 运行任务:在 Arguments: 字段中传递您的参数-Pandroid.testInstrumentationRunnerArguments.shouldSave=true Android Studio Android Instrumented Tests 运行配置: 打开运行配置,点击Instrumentation arguments:前面的...,然后添加一个name-value,如Name shouldSave Value 是的

请参阅 this article 和 this post。

⚠ 注意#7

确保使用 -ktx 版本的AndroidX Core library。-ktx 版本包含有用的 Kotlin 扩展函数:

implementation("androidx.core:core-ktx:1.6.0")

⚠ 注意#8

确保设备屏幕已打开且已解锁,以便 Activity 进入恢复状态。

代码

这是我在 src/androidTest/java/com/example/ 目录中正在测试的活动,它有一个属性指向我要截屏的视图:

class MyActivityThatContainsTheView : AppCompatActivity() 

    lateinit var myView: MyView

    override fun onCreate(savedInstanceState: Bundle?) 
        super.onCreate(savedInstanceState)
        setContentView(com.example.test.R.layout.my_layout_that_contains_the_view)
        myView = findViewById(com.example.test.R.id.my_view_id_in_the_layout_file)
    

最后,这是我的测试以及我如何保存、加载和比较屏幕截图:

@DisabledIfBuildConfigValue(named = "CI", matches = "true")
class ScreenshotTestView 

    @JvmField
    @RegisterExtension
    val scenarioExtension = ActivityScenarioExtension.launch<MyActivityThatContainsTheView>()
    lateinit var scenario: ActivityScenario<MyActivityThatContainsTheView>
    // See ⚠ Caution #6 above in the post
    val shouldSave = InstrumentationRegistry.getArguments().getString("shouldSave", "false").toBoolean()
    val shouldAssert = InstrumentationRegistry.getArguments().getString("shouldAssert", "true").toBoolean()

    @BeforeEach fun setUp() 
        scenario = scenarioExtension.scenario
        scenario.moveToState(Lifecycle.State.RESUMED)
    

    @Test fun test1() 
        val screenshotName = "screenshot-view-1"
        scenario.onActivity  activity ->
            val view = activity.myView
            view.drawToBitmap()
                    .saveIfNeeded(shouldSave, screenshotName)
                    .assertIfNeeded(shouldAssert, screenshotName)
        
    

    fun Bitmap.saveIfNeeded(shouldSave: Boolean, name: String): Bitmap 
        if (shouldSave) save(name)
        return this
    
    
    fun Bitmap.assertIfNeeded(shouldCompare: Boolean, screenshotName: String) 
        if (shouldCompare) assert(screenshotName)
    
    
    /**
     * The screenshots are saved in /Android/data/my.package.name.test/files/Pictures
     * on the external storage of the device.
     */
    private fun Bitmap.save(name: String) 
        val context = InstrumentationRegistry.getInstrumentation().targetContext
        val path = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
        val file = File(path, "$name.png")
        file.outputStream().use  stream ->
            compress(Bitmap.CompressFormat.PNG, 100, stream)
        
    
    
    private fun Bitmap.assert(screenshotName: String) 
        val reference = loadReferenceScreenshot(screenshotName)
        // I'm using AssertJ library; you can simply use assertTrue(this.sameAs(reference))
        assertThat(this.sameAs(reference))
            .withFailMessage  "Screenshots are not the same: $screenshotName.png" 
            .isTrue()
    
    
    private fun loadReferenceScreenshot(name: String): Bitmap 
        val context = InstrumentationRegistry.getInstrumentation().context
        val assets = context.resources.assets
        val reference = assets.open("compose/$name.png").use  stream ->
            BitmapFactory.decodeStream(stream)
        
        return reference
    

【讨论】:

以上是关于是否可以截屏并用浓缩咖啡测试像素值?的主要内容,如果未能解决你的问题,请参考以下文章

手机睡着的浓缩咖啡测试

带有“hasBackground”的浓缩咖啡测试

勺子和浓缩咖啡测试

指纹读取器浓缩咖啡测试

java 浓缩咖啡进口。 UI测试

java 浓缩咖啡测试 - 等待一段时间