我开始使用 espresso 并编写了一些简单的 UI 测试,用于单击按钮、输入文本并检查是否所有内容都在应有的位置。现在我想截屏并测试一些像素值,但我找不到这样做的方法。有人有什么建议吗?甚至可以用浓缩咖啡吗?
5 回答
由于 espresso 测试是简单的 InstrumentationTestCases,您可以使用狗仔队和名人来截取您的应用程序。
Spoon也是一个非常不错的截图系统。比狗仔队更清晰,但只能截取活动截图,而不是对话。
最后但同样重要的是,从 android SDK 16 开始,您可以使用screencap 实用程序对应用程序进行非常快速和清晰的屏幕截图。我认为这个解决方案确实是最好的,但它只适用于 SDK 16+。
最后针对参考屏幕截图测试屏幕截图,没有真正的工具。你可以在 stack over flow 上找到很好的实现想法,但到目前为止,还没有一个可以在 android 上使用的参考工具。
如果您正在寻找的是 Facebook 制作的用于比较屏幕截图的工具:http: //facebook.github.io/screenshot-tests-for-android/
迭代 UI 代码很困难。您如何快速验证您的布局或视图更改在所有配置中都能正常工作?screenshot-tests-for-android 可以通过提供一个测试框架来检查变化之间的视觉差异来解决这些问题。
我分叉了一个版本,将 Espresso 与 Spoon 一起运行添加到适用于 android 的优质工具中,检查一下
运行一下就会看到:
mvn clean install -P espresso-spoon
我为我的自定义视图设置屏幕截图测试付出了很多努力。
以下是我如何做到这一点以及我在此过程中学到的一切。
它也可以被常规应用程序和活动使用。
⚠ 注意#1
如果需要,您可以使用JUnit 4。我正在使用JUnit 5。因为 JUnit 5 从头开始构建在 Java 8 之上,所以它的插桩测试只能在运行 Android 8.0 (API 26) 或更新版本的设备上运行。较旧的手机或模拟器将完全跳过这些测试的执行,将它们标记为忽略。
如果您想在 Android 上运行 JUnit 5 测试,请参阅此答案以了解如何设置它。
⚠ 注意#2
屏幕截图测试可能无法在其他设备上运行,即使它们具有相同的屏幕 DPI(它们可能根本无法在具有不同屏幕 DPI 的设备上运行)。例如,即使我在本地机器和 GitHub Actions 上使用相同的设备运行测试,它们也不会产生相同的结果(GitHub Actions 断言失败)。所以,我不得不在 GitHub Actions 上禁用它们。
如果您想在 GitHub Actions 上禁用屏幕截图测试,请参阅此答案。
⚠ 注意#3
如果你在检测测试中有资源(在androidTest目录的子目录中)并且你想引用它们的 id,你应该像这样使用它们:
my.package.name.test.R.id.an_id
例如,如果您的包名称是为了在您的测试中访问src/androidTest/res/layout/my_layout.xmlcom.example
中的布局文件,则使用.com.example.test.R.layout.my_layout
⚠ 注意#4
由于我们将测试屏幕截图保存在设备/模拟器的外部存储中,我们需要确保我们在清单中添加了WRITE_EXTERNAL_STORAGE权限,并在构建脚本中配置了adb 安装选项-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目录中。因此,将该目录指定为应用构建脚本中的资产条目:
android {
sourceSets {
get("debug").assets.srcDirs("src/androidTest/assets")
}
⚠ 注意#6
在运行测试时传递检测参数(如下面shouldSave
的代码):shouldAssert
- 摇篮任务:
- 从命令行运行任务:在任务名称后传递参数
./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 true。
⚠ 注意#7
确保使用AndroidX Core library的-ktx版本。
-ktx版本包含有用的 Kotlin 扩展函数:
implementation("androidx.core:core-ktx:1.6.0")
⚠ 注意#8
确保设备屏幕已打开并已解锁,以使活动进入恢复状态。
编码
这是我在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
}
}
作为进行屏幕截图比较的替代方法,您可以考虑使用LayoutVerifier等库来比较布局状态本身(即视图的位置)。
虽然它不会以屏幕截图的形式为您提供布局的可视化表示,但它本质上通过比较视图的状态来执行相同的功能。它还可以在 Robolectric 上运行,无需在真实设备上运行。