Bibliothèques et outils pour tester différentes tailles d'écran

Android propose différents outils et API qui peuvent vous aider à créer des tests pour différentes tailles d'écran et de fenêtre.

DeviceConfigurationOverride

Le composable DeviceConfigurationOverride vous permet de remplacer les attributs de configuration pour tester plusieurs tailles d'écran et de fenêtre dans les mises en page Compose. Le forçage ForcedSize s'adapte à n'importe quelle mise en page dans l'espace disponible, ce qui vous permet d'exécuter n'importe quel test d'interface utilisateur sur n'importe quelle taille d'écran. Par exemple, vous pouvez utiliser un petit facteur de forme de téléphone pour exécuter tous vos tests d'interface utilisateur, y compris les tests d'interface utilisateur pour les grands téléphones, les téléphones pliables et les tablettes.

   DeviceConfigurationOverride(
        DeviceConfigurationOverride.ForcedSize(DpSize(1280.dp, 800.dp))
    ) {
        MyScreen() // Will be rendered in the space for 1280dp by 800dp without clipping.
    }
Figure 1. Utilisation de DeviceConfigurationOverride pour adapter une mise en page de tablette à un appareil de plus petit format, comme dans \*Now in Android* (Désormais disponible sur Android).

De plus, vous pouvez utiliser ce composable pour définir l'échelle de la police, les thèmes et d'autres propriétés que vous souhaitez tester sur différentes tailles de fenêtre.

Robolectric

Utilisez Robolectric pour exécuter des tests d'interface utilisateur basés sur Compose ou sur les vues sur la JVM localement, sans appareil ni émulateur. Vous pouvez configurer Robolectric pour qu'il utilise des tailles d'écran spécifiques, entre autres propriétés utiles.

Dans l'exemple suivant de Now in Android, Robolectric est configuré pour émuler une taille d'écran de 1 000 x 1 000 dp avec une résolution de 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 { ... }

Vous pouvez également définir les qualificatifs à partir du corps du test, comme dans cet extrait de l'exemple Now in Android:

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

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

Notez que RuntimeEnvironment.setQualifiers() met à jour les ressources système et d'application avec la nouvelle configuration, mais ne déclenche aucune action sur les activités actives ni sur d'autres composants.

Pour en savoir plus, consultez la documentation Configuration de l'appareil de Robolectric.

Appareils gérés par Gradle

Le plug-in Android Gradle pour les appareils gérés par Gradle (GMD) vous permet de définir les spécifications des émulateurs et des appareils réels sur lesquels vos tests d'instrumentation s'exécutent. Créez des spécifications pour les appareils avec différentes tailles d'écran afin d'implémenter une stratégie de test selon laquelle certains tests doivent être exécutés sur certaines tailles d'écran. En utilisant GMD avec l'intégration continue (CI), vous pouvez vous assurer que les tests appropriés s'exécutent lorsque cela est nécessaire, en provisionnant et en lançant des émulateurs, et en simplifiant la configuration de votre 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"
                }
            }
        }
    }
}

Vous trouverez plusieurs exemples de GMD dans le projet testing-samples.

Firebase Test Lab

Utilisez Firebase Test Lab (FTL) ou un service de ferme d'appareils similaire pour exécuter vos tests sur des appareils physiques spécifiques auxquels vous n'avez peut-être pas accès, tels que des appareils pliables ou des tablettes de différentes tailles. Firebase Test Lab est un service payant avec un niveau sans frais. FTL permet également d'exécuter des tests sur des émulateurs. Ces services améliorent la fiabilité et la rapidité des tests d'instrumentation, car ils peuvent provisionner des appareils et des émulateurs à l'avance.

Pour en savoir plus sur l'utilisation de FTL avec GMD, consultez Adapter vos tests grâce aux appareils gérés par Gradle.

Tester le filtrage avec le testeur

Une stratégie de test optimale ne doit pas vérifier la même chose deux fois. Par conséquent, la plupart de vos tests d'interface utilisateur n'ont pas besoin d'être exécutés sur plusieurs appareils. En règle générale, vous filtrez vos tests d'interface utilisateur en les exécutant tous ou la plupart sur un facteur de forme de téléphone et seulement un sous-ensemble sur des appareils de différentes tailles d'écran.

Vous pouvez annoter certains tests pour qu'ils ne soient exécutés qu'avec certains appareils, puis transmettre un argument à AndroidJUnitRunner à l'aide de la commande qui exécute les tests.

Par exemple, vous pouvez créer différentes annotations:

annotation class TestExpandedWidth
annotation class TestCompactWidth

Et les utiliser dans différents tests:

class MyTestClass {

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

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

}

Vous pouvez ensuite utiliser la propriété android.testInstrumentationRunnerArguments.annotation lorsque vous exécutez les tests pour filtrer des tests spécifiques. Par exemple, si vous utilisez des appareils gérés par Gradle:

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

Si vous n'utilisez pas GMD et que vous gérez des émulateurs sur un IC, assurez-vous d'abord que l'émulateur ou l'appareil approprié est prêt et connecté, puis transmettez le paramètre à l'une des commandes Gradle pour exécuter des tests instrumentés:

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

Notez que l'appareil Espresso (voir la section suivante) peut également filtrer les tests à l'aide des propriétés de l'appareil.

Appareil Espresso

Utilisez Espresso Device pour effectuer des actions sur les émulateurs dans les tests utilisant n'importe quel type de tests d'instrumentation, y compris les tests Espresso, Compose ou UI Automator. Ces actions peuvent inclure le paramétrage de la taille de l'écran ou l'activation/la désactivation des états ou des positions pliables. Par exemple, vous pouvez contrôler un émulateur pliable et le définir en mode sur table. L'appareil Espresso contient également des règles et des annotations JUnit pour exiger certaines fonctionnalités:

@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()))
    }
}

Notez que l'appareil Espresso est toujours en phase alpha et présente les exigences suivantes:

  • Plug-in Android Gradle 8.3 ou version ultérieure
  • Android Emulator 33.1.10 ou version ultérieure
  • Un appareil virtuel Android exécutant le niveau d'API 24 ou supérieur

Filtrer les tests

Espresso Device peut lire les propriétés des appareils connectés pour vous permettre de filtrer les tests à l'aide d'annotations. Si les exigences annotées ne sont pas respectées, les tests sont ignorés.

Annotation RequiresDeviceMode

L'annotation RequiresDeviceMode peut être utilisée plusieurs fois pour indiquer un test qui ne s'exécutera que si toutes les valeurs DeviceMode sont compatibles avec l'appareil.

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()))
    }
}

Annotation RequiresDisplay

L'annotation RequiresDisplay vous permet de spécifier la largeur et la hauteur de l'écran de l'appareil à l'aide de classes de taille, qui définissent des buckets de dimension conformément aux classes de taille de fenêtre officielles.

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

Redimensionner les écrans

Utilisez la méthode setDisplaySize() pour redimensionner les dimensions de l'écran au moment de l'exécution. Utilisez la méthode avec la classe DisplaySizeRule, qui s'assure que toutes les modifications apportées lors des tests sont annulées avant le test suivant.

@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.
    }
}

Lorsque vous redimensionnez un écran avec setDisplaySize(), vous n'affectez pas la densité de l'appareil. Par conséquent, si une dimension ne rentre pas dans l'appareil cible, le test échoue avec une erreur UnsupportedDeviceOperationException. Pour empêcher l'exécution des tests dans ce cas, utilisez l'annotation RequiresDisplay pour les filtrer:

@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

La classe StateRestorationTester permet de tester la restauration de l'état des composants composables sans recréer d'activités. Cela rend les tests plus rapides et plus fiables, car la recréation d'activité est un processus complexe avec plusieurs mécanismes de synchronisation:

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

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

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

Bibliothèque Window Testing

La bibliothèque de test des fenêtres contient des utilitaires pour vous aider à écrire des tests qui s'appuient sur des fonctionnalités liées à la gestion des fenêtres ou qui les vérifient, telles que l'intégration d'activités ou les fonctionnalités pliables. L'artefact est disponible dans le dépôt Maven de Google.

Par exemple, vous pouvez utiliser la fonction FoldingFeature() pour générer un FoldingFeature personnalisé, que vous pouvez utiliser dans les aperçus Compose. En Java, utilisez la fonction createFoldingFeature().

Dans un aperçu Compose, vous pouvez implémenter FoldingFeature comme suit:

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

Vous pouvez également émuler les fonctionnalités d'affichage dans les tests d'interface utilisateur à l'aide de la fonction TestWindowLayoutInfo(). L'exemple suivant simule une FoldingFeature avec une charnière verticale HALF_OPENED au centre de l'écran, puis vérifie si la mise en page est bien celle attendue:

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()
    }
}

Vues

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()))
    }
}

Vous trouverez d'autres exemples dans le projet WindowManager.

Ressources supplémentaires

Documentation

Exemples

Ateliers de programmation