Библиотеки и инструменты для тестирования разных размеров экрана

В Android есть множество инструментов и API, которые помогут вам создать тесты для разных размеров экрана и окна.

DeviceConfigurationOverride

Composable-функция 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.

Кроме того, вы можете использовать эту composable-функцию, чтобы задать масштаб шрифта, темы и другие свойства, которые вы хотите протестировать на разных размерах окон.

Robolectric

Используйте Robolectric, чтобы запускать тесты Compose или интерфейса на основе представлений на JVM локально – устройства или эмуляторы не требуются. Вы можете настроить Robolectric так, чтобы он использовал определенные размеры экрана и другие полезные свойства.

В следующем примере из Now in Android Robolectric настроен на эмуляцию экрана размером 1000 x 1000 dp с разрешением 480 dpi:

@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() обновляет системные ресурсы и ресурсы приложения с новой конфигурацией, но не запускает никаких действий в активных действиях или других компонентах.

Подробную информацию можно найти в документации Robolectric по конфигурации устройств.

Устройства под управлением Gradle

Плагин Android Gradle для устройств под управлением 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"
                }
            }
        }
    }
}

Вы можете найти несколько примеров GMD в проекте testing-samples.

Firebase Test Lab

Используйте Firebase Test Lab или аналогичный сервис, чтобы тестировать приложение на реальных устройствах, к которым у вас нет доступа, например на складных телефонах или планшетах разных размеров. 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 Device (см. следующий раздел) также может фильтровать тесты по свойствам устройства.

Устройство Espresso

Используйте Espresso Device, чтобы выполнять действия на эмуляторах в тестах, используя любой тип инструментальных тестов, включая тесты Espresso, Compose или UI Automator. Например, можно задать размер экрана или переключить состояние складного устройства. Например, вы можете управлять эмулятором складного устройства и перевести его в настольный режим. Espresso Device также содержит правила и аннотации 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 все ещё находится на стадии альфа-тестирования и имеет следующие требования:

  • Плагин Android Gradle 8.3 или более поздней версии.
  • Android Emulator 33.1.10 или более поздней версии;
  • Виртуальное устройство Android с API уровня 24 или выше.

Фильтровать тесты

Espresso Device может считывать свойства подключенных устройств, чтобы вы могли фильтровать тесты с помощью аннотаций. Если требования, указанные в аннотации, не выполняются, тесты пропускаются.

Аннотация RequiresDeviceMode

Аннотацию 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()))
    }
}

Аннотация RequiresDisplay

Аннотация 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 используется для тестирования восстановления состояния composable-функций без повторного создания 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

Библиотека Window Testing содержит утилиты, которые помогают писать тесты, основанные на функциях, связанных с управлением окнами, или проверяющие их. К таким функциям относятся, например, встраивание действий и функции для складных устройств. Артефакт доступен в репозитории Google Maven.

Например, вы можете использовать функцию FoldingFeature(), чтобы создать собственный FoldingFeature, который можно использовать в предварительном просмотре Compose. В 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(). В следующем примере симулируется устройство FoldingFeature с вертикальным шарниром HALF_OPENED в центре экрана, а затем проверяется, соответствует ли макет ожидаемому:

Написать

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.

Дополнительные ресурсы

Документация

Образцы

Практические работы