다양한 화면 크기를 테스트하는 라이브러리 및 도구

Android는 다양한 화면 및 창 크기에 맞는 테스트를 만드는 데 도움이 되는 다양한 도구와 API를 제공합니다.

DeviceConfigurationOverride

DeviceConfigurationOverride 컴포저블을 사용하면 구성 속성을 재정의하여 Compose 레이아웃에서 여러 화면 및 창 크기를 테스트할 수 있습니다. ForcedSize 재정의는 사용 가능한 공간에 모든 레이아웃을 맞추므로 모든 화면 크기에서 UI 테스트를 실행할 수 있습니다. 예를 들어 소형 휴대전화 폼 팩터를 사용하여 대형 휴대전화, 폴더블, 태블릿의 UI 테스트를 비롯한 모든 UI 테스트를 실행할 수 있습니다.

   DeviceConfigurationOverride(
        DeviceConfigurationOverride.ForcedSize(DpSize(1280.dp, 800.dp))
    ) {
        MyScreen() // Will be rendered in the space for 1280dp by 800dp without clipping.
    }
그림 1. \*Now in Android*와 같이 DeviceConfigurationOverride를 사용하여 태블릿 레이아웃을 소형 폼 팩터 기기에 맞춥니다.

또한 이 컴포저블을 사용하여 다양한 창 크기에서 테스트할 글꼴 크기, 테마, 기타 속성을 설정할 수 있습니다.

Robolectric

Robolectric을 사용하여 JVM에서 Compose 또는 뷰 기반 UI 테스트를 로컬에서 실행합니다. 기기나 에뮬레이터가 필요하지 않습니다. Robolectric을 구성하여 다른 유용한 속성 중에서 특정 화면 크기를 사용하도록 구성할 수 있습니다.

Now in Android의 다음 에서 Robolectric은 해상도가 480dpi인 1000x1000dp 화면 크기를 에뮬레이션하도록 구성되어 있습니다.

@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 관리 기기

Gradle 관리 기기 (GMD) Android Gradle 플러그인을 사용하면 계측된 테스트가 실행되는 에뮬레이터 및 실제 기기의 사양을 정의할 수 있습니다. 특정 테스트를 특정 화면 크기에서 실행해야 하는 테스트 전략을 구현하기 위해 화면 크기가 다른 기기의 사양을 만듭니다. 지속적 통합(CI)과 함께 GMD를 사용하면 필요할 때 적절한 테스트가 실행되도록 하고, 에뮬레이터를 프로비저닝 및 실행하고, 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 Test Lab

Firebase Test Lab(FTL) 또는 유사한 기기 농장 서비스를 사용하여 액세스할 수 없는 특정 실제 기기(예: 폴더블, 다양한 크기의 태블릿)에서 테스트를 실행합니다. Firebase Test Lab은 무료 등급이 있는 유료 서비스입니다. FTL은 에뮬레이터에서 테스트 실행도 지원합니다. 이러한 서비스는 기기와 에뮬레이터를 미리 프로비저닝할 수 있으므로 계측 테스트의 안정성과 속도를 개선합니다.

GMD에서 FTL을 사용하는 방법에 관한 자세한 내용은 Gradle 관리 기기로 테스트 확장을 참고하세요.

테스트 실행기로 테스트 필터링

최적의 테스트 전략은 동일한 항목을 두 번 확인해서는 안 되므로 대부분의 UI 테스트는 여러 기기에서 실행할 필요가 없습니다. 일반적으로 UI 테스트의 일부 또는 전부를 휴대전화 폼 팩터에서 실행하고 화면 크기가 다른 기기에서는 하위 집합만 실행하여 UI 테스트를 필터링합니다.

특정 기기에서만 실행되도록 특정 테스트에 주석을 추가한 다음 테스트를 실행하는 명령어를 사용하여 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 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 이상
  • API 수준 24 이상을 실행하는 Android 가상 기기

테스트 필터링

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 클래스는 활동을 다시 만들지 않고 컴포저블 구성요소의 상태 복원을 테스트하는 데 사용됩니다. 이렇게 하면 활동 재생성이 여러 동기화 메커니즘이 있는 복잡한 프로세스이므로 테스트가 더 빠르고 안정적으로 실행됩니다.

@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 테스트 라이브러리

Window Testing 라이브러리에는 활동 삽입 또는 폴더블 기능과 같이 창 관리와 관련된 기능을 사용하거나 확인하는 테스트를 작성하는 데 도움이 되는 유틸리티가 포함되어 있습니다. 아티팩트는 Google Maven 저장소를 통해 사용할 수 있습니다.

예를 들어 FoldingFeature() 함수를 사용하여 맞춤 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() 함수를 사용하여 UI 테스트에서 디스플레이 기능을 에뮬레이션할 수 있습니다. 다음 예에서는 화면 중앙에 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