用于测试不同屏幕尺寸的库和工具

Android 提供了各种工具和 API,可帮助您针对不同屏幕尺寸和窗口大小创建测试。

DeviceConfigurationOverride

借助 DeviceConfigurationOverride 可组合项,您可以替换配置属性,以在 Compose 布局中测试多种屏幕尺寸和窗口大小。ForcedSize 替换项适合可用空间中的任何布局,可让您在任何屏幕尺寸上运行任何界面测试。例如,您可以使用小尺寸的手机来运行所有界面测试,包括针对大手机、可折叠设备和平板电脑的界面测试。

   DeviceConfigurationOverride(
        DeviceConfigurationOverride.ForcedSize(DpSize(1280.dp, 800.dp))
    ) {
        MyScreen() // Will be rendered in the space for 1280dp by 800dp without clipping.
    }
图 1.使用 DeviceConfigurationOverride 让平板电脑布局适合尺寸较小的设备(如 \*Now in Android*)。

此外,您还可以使用此可组合项来设置可能需要针对不同窗口大小测试的字体比例、主题背景和其他属性。

Robolectric

使用 Robolectric 在 JVM 上本地运行 Compose 或基于视图的界面测试,无需设备或模拟器。您可以配置 Robolectric,以使用特定屏幕尺寸以及其他实用属性。

Now in Android 的以下示例中,Robolectric 配置为模拟分辨率为 480 dpi 的 1000x1000 dp 的屏幕尺寸:

@RunWith(RobolectricTestRunner::class)
// Configure Robolectric to use a very large screen size that can fit all of the test sizes.
// This allows enough room to render the content under test without clipping or scaling.
@Config(qualifiers = "w1000dp-h1000dp-480dpi")
class NiaAppScreenSizesScreenshotTests { ... }

您还可以在测试正文中设置限定符,如 Now in Android 示例中的以下代码段所示:

val (width, height, dpi) = ...

// Set qualifiers from specs.
RuntimeEnvironment.setQualifiers("w${width}dp-h${height}dp-${dpi}dpi")

请注意,RuntimeEnvironment.setQualifiers() 会使用新配置更新系统和应用资源,但不会触发对活跃 activity 或其他组件的任何操作。

如需了解详情,请参阅 Robolectric 设备配置文档。

Gradle 管理的设备

借助 Gradle 管理的设备 (GMD) Android Gradle 插件,您可以定义运行插桩测试的模拟器和真实设备的规格。为具有不同屏幕尺寸的设备创建规范,以实现某种测试策略,在这种策略中,某些测试必须在特定的屏幕尺寸上运行。通过将 GMD 与持续集成 (CI) 结合使用,您可以确保根据需要运行适当的测试,配置和启动模拟器,以及简化 CI 设置。

android {
    testOptions {
        managedDevices {
            devices {
                // Run with ./gradlew nexusOneApi30DebugAndroidTest.
                nexusOneApi30(com.android.build.api.dsl.ManagedVirtualDevice) {
                    device = "Nexus One"
                    apiLevel = 30
                    // Use the AOSP ATD image for better emulator performance
                    systemImageSource = "aosp-atd"
                }
                // Run with ./gradlew  foldApi34DebugAndroidTest.
                foldApi34(com.android.build.api.dsl.ManagedVirtualDevice) {
                    device = "Pixel Fold"
                    apiLevel = 34
                    systemImageSource = "aosp-atd"
                }
            }
        }
    }
}

您可以在 testing-samples 项目中找到多个 GMD 示例。

Firebase 测试实验室

您可以使用 Firebase Test Lab (FTL) 或类似的设备场服务,在您可能无法访问的特定真实设备(例如不同大小的可折叠设备或平板电脑)上运行测试。Firebase Test Lab 是一项付费服务,提供免费层级。FTL 还支持在模拟器上运行测试。这些服务可以提前配置设备和模拟器,因此可以提高插桩测试的可靠性和速度。

如需了解如何将 FTL 与 GMD 搭配使用,请参阅使用 Gradle 管理的设备扩展测试

使用测试运行程序进行测试过滤

最佳的测试策略不应该对同一件事进行两次验证,因此大多数界面测试都不需要在多个设备上运行。通常,您可以通过在手机外形规格上运行所有或大部分界面测试,在具有不同屏幕尺寸的设备上仅运行其中一部分测试来过滤界面测试。

您可以为某些测试添加注解,使其仅在特定设备上运行,然后使用运行测试的命令将参数传递给 AndroidJUnitRunner

例如,您可以创建不同的注释:

annotation class TestExpandedWidth
annotation class TestCompactWidth

并在不同的测试中使用它们:

class MyTestClass {

    @Test
    @TestExpandedWidth
    fun myExample_worksOnTablet() {
        ...
    }

    @Test
    @TestCompactWidth
    fun myExample_worksOnPortraitPhone() {
        ...
    }

}

然后,您可以在运行测试时使用 android.testInstrumentationRunnerArguments.annotation 属性来过滤特定测试。例如,如果您使用的是 Gradle 管理的设备:

$ ./gradlew pixelTabletApi30DebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.annotation='com.sample.TestExpandedWidth'

如果您不使用 GMD 并在 CI 上管理模拟器,请先确保正确的模拟器或设备已准备就绪并已连接,然后将该参数传递给某个 Gradle 命令以运行插桩测试:

$ ./gradlew connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.annotation='com.sample.TestExpandedWidth'

请注意,Espresso 设备(请参阅下一部分)还可以使用设备属性过滤测试。

浓缩咖啡设备

使用 Espresso 设备在使用任何类型的插桩测试(包括 Espresso、Compose 或 UI Automator 测试)的测试中对模拟器执行操作。这些操作可能包括设置屏幕尺寸或切换可折叠设备状态或折叠状态。例如,您可以控制可折叠设备模拟器,并将其设置为桌面模式。Espresso 设备还包含用于需要特定功能的 JUnit 规则和注解:

@RunWith(AndroidJUnit4::class)
class OnDeviceTest {

    @get:Rule(order=1) val activityScenarioRule = activityScenarioRule<MainActivity>()

    @get:Rule(order=2) val screenOrientationRule: ScreenOrientationRule =
        ScreenOrientationRule(ScreenOrientation.PORTRAIT)

    @Test
    fun tabletopMode_playerIsDisplayed() {
        // Set the device to tabletop mode.
        onDevice().setTabletopMode()
        onView(withId(R.id.player)).check(matches(isDisplayed()))
    }
}

请注意,Espresso Device 仍处于 Alpha 版阶段,具有以下要求:

  • Android Gradle 插件 8.3 或更高版本
  • Android 模拟器 33.1.10 或更高版本
  • 搭载 API 级别 24 或更高级别的 Android 虚拟设备

过滤测试

Espresso 设备可以读取已连接设备的属性,让您能够使用注解过滤测试。如果不符合带注解的要求,则会跳过测试。

RequireDeviceMode 注解

RequiresDeviceMode 注解可以多次用于指示仅当设备支持所有 DeviceMode 值时才会运行的测试。

class OnDeviceTest {
    ...
    @Test
    @RequiresDeviceMode(TABLETOP)
    @RequiresDeviceMode(BOOK)
    fun tabletopMode_playerIdDisplayed() {
        // Set the device to tabletop mode.
        onDevice().setTabletopMode()
        onView(withId(R.id.player)).check(matches(isDisplayed()))
    }
}

RequireDisplay 注解

借助 RequiresDisplay 注解,您可以使用尺寸类别指定设备屏幕的宽度和高度,这些尺寸类按照官方窗口大小类别定义尺寸范围。

class OnDeviceTest {
    ...
    @Test
    @RequiresDisplay(EXPANDED, COMPACT)
    fun myScreen_expandedWidthCompactHeight() {
        ...
    }
}

调整显示屏大小

使用 setDisplaySize() 方法可在运行时调整屏幕的尺寸。请将此方法与 DisplaySizeRule 类结合使用,该类可确保测试期间所做的任何更改都在下次测试之前撤消。

@RunWith(AndroidJUnit4::class)
class ResizeDisplayTest {

    @get:Rule(order = 1) val activityScenarioRule = activityScenarioRule<MainActivity>()

    // Test rule for restoring device to its starting display size when a test case finishes.
    @get:Rule(order = 2) val displaySizeRule: DisplaySizeRule = DisplaySizeRule()

    @Test
    fun resizeWindow_compact() {
        onDevice().setDisplaySize(
            widthSizeClass = WidthSizeClass.COMPACT,
            heightSizeClass = HeightSizeClass.COMPACT
        )
        // Verify visual attributes or state restoration.
    }
}

使用 setDisplaySize() 调整屏幕大小时,不会影响设备的密度,因此如果某个尺寸不适合目标设备,测试会失败并显示 UnsupportedDeviceOperationException。在这种情况下,为阻止运行测试,请使用 RequiresDisplay 注解将其过滤掉:

@RunWith(AndroidJUnit4::class)
class ResizeDisplayTest {

    @get:Rule(order = 1) var activityScenarioRule = activityScenarioRule<MainActivity>()

    // Test rule for restoring device to its starting display size when a test case finishes.
    @get:Rule(order = 2) var displaySizeRule: DisplaySizeRule = DisplaySizeRule()

    /**
     * Setting the display size to EXPANDED would fail in small devices, so the [RequiresDisplay]
     * annotation prevents this test from being run on devices outside the EXPANDED buckets.
     */
    @RequiresDisplay(
        widthSizeClass = WidthSizeClassEnum.EXPANDED,
        heightSizeClass = HeightSizeClassEnum.EXPANDED
    )
    @Test
    fun resizeWindow_expanded() {
        onDevice().setDisplaySize(
            widthSizeClass = WidthSizeClass.EXPANDED,
            heightSizeClass = HeightSizeClass.EXPANDED
        )
        // Verify visual attributes or state restoration.
    }
}

StateRestorationTester

StateRestorationTester 类用于在不重新创建 activity 的情况下测试可组合组件的状态恢复。由于 activity 重新创建是一个涉及多种同步机制的复杂过程,因此可使测试更快且更可靠:

@Test
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {
    val stateRestorationTester = StateRestorationTester(composeTestRule)

    // Set content through the StateRestorationTester object.
    stateRestorationTester.setContent {
        MyApp()
    }

    // Simulate a config change.
    stateRestorationTester.emulateSavedInstanceStateRestore()
}

窗口测试库

Window Testing 库包含一些实用程序,可帮助您编写依赖于或验证与窗口管理相关的功能(如 activity 嵌入或可折叠功能)的测试。该工件可通过 Google 的 Maven 制品库获取。

例如,您可以使用 FoldingFeature() 函数生成可在 Compose 预览中使用的自定义 FoldingFeature。在 Java 中,请使用 createFoldingFeature() 函数。

在 Compose 预览中,您可以通过以下方式实现 FoldingFeature

@Preview(showBackground = true, widthDp = 480, heightDp = 480)
@Composable private fun FoldablePreview() =
    MyApplicationTheme {
        ExampleScreen(
            displayFeatures = listOf(FoldingFeature(Rect(0, 240, 480, 240)))
        )
 }

此外,您还可以使用 TestWindowLayoutInfo() 函数在界面测试中模拟显示功能。以下示例模拟了屏幕中心的 HALF_OPENED 垂直合页的 FoldingFeature,然后检查布局是否符合预期:

Compose

import androidx.window.layout.FoldingFeature.Orientation.Companion.VERTICAL
import androidx.window.layout.FoldingFeature.State.Companion.HALF_OPENED
import androidx.window.testing.layout.FoldingFeature
import androidx.window.testing.layout.TestWindowLayoutInfo
import androidx.window.testing.layout.WindowLayoutInfoPublisherRule

@RunWith(AndroidJUnit4::class)
class MediaControlsFoldingFeatureTest {

    @get:Rule(order=1)
    val composeTestRule = createAndroidComposeRule<ComponentActivity>()

    @get:Rule(order=2)
    val windowLayoutInfoPublisherRule = WindowLayoutInfoPublisherRule()

    @Test
    fun foldedWithHinge_foldableUiDisplayed() {
        composeTestRule.setContent {
            MediaPlayerScreen()
        }

        val hinge = FoldingFeature(
            activity = composeTestRule.activity,
            state = HALF_OPENED,
            orientation = VERTICAL,
            size = 2
        )

        val expected = TestWindowLayoutInfo(listOf(hinge))
        windowLayoutInfoPublisherRule.overrideWindowLayoutInfo(expected)

        composeTestRule.waitForIdle()

        // Verify that the folding feature is detected and media controls shown.
        composeTestRule.onNodeWithTag("MEDIA_CONTROLS").assertExists()
    }
}

观看次数

import androidx.window.layout.FoldingFeature.Orientation
import androidx.window.layout.FoldingFeature.State
import androidx.window.testing.layout.FoldingFeature
import androidx.window.testing.layout.TestWindowLayoutInfo
import androidx.window.testing.layout.WindowLayoutInfoPublisherRule

@RunWith(AndroidJUnit4::class)
class MediaControlsFoldingFeatureTest {

    @get:Rule(order=1)
    val activityRule = ActivityScenarioRule(MediaPlayerActivity::class.java)

    @get:Rule(order=2)
    val windowLayoutInfoPublisherRule = WindowLayoutInfoPublisherRule()

    @Test
    fun foldedWithHinge_foldableUiDisplayed() {
        activityRule.scenario.onActivity { activity ->
            val feature = FoldingFeature(
                activity = activity,
                state = State.HALF_OPENED,
                orientation = Orientation.VERTICAL)
            val expected = TestWindowLayoutInfo(listOf(feature))
            windowLayoutInfoPublisherRule.overrideWindowLayoutInfo(expected)
        }

        // Verify that the folding feature is detected and media controls shown.
        onView(withId(R.id.media_controls)).check(matches(isDisplayed()))
    }
}

您可以在 WindowManager 项目中找到更多示例。

其他资源

文档

示例

Codelab