Bibliotecas e ferramentas para testar diferentes tamanhos de tela

O Android oferece várias ferramentas e APIs que podem ajudar você a criar testes para diferentes tamanhos de tela e janela.

DeviceConfigurationOverride

Com o elemento combinável DeviceConfigurationOverride, é possível substituir atributos de configuração para testar vários tamanhos de tela e janela em layouts do Compose. A substituição ForcedSize se ajusta a qualquer layout no espaço disponível, o que permite executar qualquer teste de interface em qualquer tamanho de tela. Por exemplo, você pode usar um smartphone pequeno para executar todos os testes de interface, incluindo os de smartphones grandes, dobráveis e tablets.

   DeviceConfigurationOverride(
        DeviceConfigurationOverride.ForcedSize(DpSize(1280.dp, 800.dp))
    ) {
        MyScreen() // Will be rendered in the space for 1280dp by 800dp without clipping.
    }
Figura 1. Usar DeviceConfigurationOverride para ajustar um layout de tablet em um dispositivo menor, como em \*Now in Android*.

Além disso, você pode usar esse elemento combinável para definir escala de fonte, temas e outras propriedades que talvez queira testar em diferentes tamanhos de janela.

Robolectric

Use o Robolectric para executar testes de UI baseados em visualizações ou do Compose na JVM localmente. Não é necessário usar dispositivos ou emuladores. É possível configurar o Robolectric para usar tamanhos de tela específicos, entre outras propriedades úteis.

No exemplo a seguir do Now in Android, o Robolectric está configurado para emular um tamanho de tela de 1000 x 1000 dp com uma resolução 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 { ... }

Também é possível definir os qualificadores do corpo do teste, como feito neste snippet do exemplo Now in Android:

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

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

O RuntimeEnvironment.setQualifiers() atualiza o sistema e os recursos do aplicativo com a nova configuração, mas não aciona nenhuma ação em atividades ativas ou outros componentes.

Leia mais na documentação de Configuração do dispositivo do Robolectric.

Dispositivos gerenciados pelo Gradle

O plug-in do Android Gradle para dispositivos gerenciados pelo Gradle (GMD) permite definir as especificações dos emuladores e dispositivos reais em que os testes instrumentados são executados. Crie especificações para dispositivos com diferentes tamanhos de tela para implementar uma estratégia de teste em que determinados testes precisam ser executados em tamanhos de tela específicos. Ao usar o GMD com integração contínua (CI), você garante que os testes adequados sejam executados quando necessário, provisionando e iniciando emuladores e simplificando a configuração da 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"
                }
            }
        }
    }
}

Você pode encontrar vários exemplos de GMD no projeto testing-samples.

Firebase Test Lab

Use o Firebase Test Lab (FTL) ou um serviço semelhante de farm de dispositivos para executar seus testes em dispositivos reais específicos a que você não tem acesso, como dispositivos dobráveis ou tablets de tamanhos variados. O Firebase Test Lab é um serviço pago com um nível sem custo financeiro. A FTL também permite executar testes em emuladores. Esses serviços melhoram a confiabilidade e a velocidade dos testes instrumentados porque podem provisionar dispositivos e emuladores com antecedência.

Para informações sobre como usar o FTL com GMD, consulte Escalonar seus testes com dispositivos gerenciados pelo Gradle.

Filtragem de testes com o executor de testes

Uma estratégia de teste ideal não verifica a mesma coisa duas vezes. Portanto, a maioria dos seus testes de interface não precisa ser executada em vários dispositivos. Normalmente, você filtra os testes de UI executando todos ou a maioria deles em um formato de smartphone e apenas um subconjunto em dispositivos com tamanhos de tela diferentes.

É possível anotar alguns testes para serem executados apenas com determinados dispositivos e transmitir um argumento ao AndroidJUnitRunner usando o comando que executa os testes.

Por exemplo, você pode criar anotações diferentes:

annotation class TestExpandedWidth
annotation class TestCompactWidth

e use-os em testes diferentes:

class MyTestClass {

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

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

}

Depois, use a propriedade android.testInstrumentationRunnerArguments.annotation ao executar os testes para filtrar os específicos. Por exemplo, se você estiver usando dispositivos gerenciados pelo Gradle:

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

Se você não usa o GMD e gerencia emuladores na CI, primeiro verifique se o emulador ou dispositivo correto está pronto e conectado. Depois, transmita o parâmetro para um dos comandos do Gradle para executar testes instrumentados:

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

O Espresso Device (consulte a próxima seção) também pode filtrar testes usando propriedades do dispositivo.

Dispositivo Espresso

Use o Espresso Device para realizar ações em emuladores em testes usando qualquer tipo de teste instrumentado, incluindo testes do Espresso, do Compose ou do UI Automator. Essas ações podem incluir a definição do tamanho da tela ou a alternância entre estados ou posturas dobráveis. Por exemplo, você pode controlar um emulador dobrável e definir o modo de mesa. O Espresso Device também contém regras e anotações do JUnit para exigir determinados recursos:

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

O Espresso Device ainda está na fase Alfa e tem os seguintes requisitos:

  • Plug-in do Android para Gradle 8.3 ou mais recente
  • Android Emulator 33.1.10 ou versão mais recente
  • Dispositivo virtual Android que executa a API de nível 24 ou mais recente

Filtrar testes

O Espresso Device pode ler as propriedades dos dispositivos conectados para permitir que você filtre testes usando anotações. Se os requisitos anotados não forem atendidos, os testes serão ignorados.

Anotação RequiresDeviceMode

A anotação RequiresDeviceMode pode ser usada várias vezes para indicar um teste que será executado somente se todos os valores de DeviceMode forem compatíveis no dispositivo.

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

Anotação RequiresDisplay

Com a anotação RequiresDisplay, é possível especificar a largura e a altura da tela do dispositivo usando classes de tamanho, que definem intervalos de dimensões seguindo as classes de tamanho de janela oficiais.

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

Redimensionar telas

Use o método setDisplaySize() para redimensionar as dimensões da tela durante a execução. Use o método com a classe DisplaySizeRule, que garante que todas as mudanças feitas durante os testes sejam desfeitas antes do próximo teste.

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

Ao redimensionar uma tela com setDisplaySize(), você não afeta a densidade do dispositivo. Portanto, se uma dimensão não couber no dispositivo de destino, o teste vai falhar com um UnsupportedDeviceOperationException. Para evitar que os testes sejam executados nesse caso, use a anotação RequiresDisplay para filtrá-los:

@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

A classe StateRestorationTester é usada para testar a restauração de estado de componentes combináveis sem recriar atividades. Isso torna os testes mais rápidos e confiáveis, já que a recriação de atividades é um processo complexo com vários mecanismos de sincronização:

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

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

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

Biblioteca de testes de janela

A biblioteca Window Testing contém utilitários para ajudar você a escrever testes que dependem ou verificam recursos relacionados ao gerenciamento de janelas, como incorporação de atividades ou recursos dobráveis. O artefato está disponível no repositório Maven do Google.

Por exemplo, é possível usar a função FoldingFeature() para gerar um FoldingFeature personalizado, que pode ser usado nas prévias do Compose. Em Java, use a função createFoldingFeature().

Em uma prévia do Compose, é possível implementar o FoldingFeature da seguinte maneira:

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

Além disso, é possível emular recursos de exibição em testes de interface usando a função TestWindowLayoutInfo(). O exemplo a seguir simula um FoldingFeature com uma articulação vertical HALF_OPENED no centro da tela e verifica se o layout está de acordo com o esperado:

Escrever

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

Visualizações

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

Encontre mais exemplos no projeto WindowManager.

Outros recursos

Documentação

Amostras

Codelabs