Google se compromete a impulsar la igualdad racial para las comunidades afrodescendientes. Obtén información al respecto.

Cómo probar tu diseño de Compose

Las pruebas de una IU basada en Compose son distintas a las de una IU basada en vistas. El kit de herramientas de la IU basada en vistas define claramente qué es una vista. Las vistas ocupan un espacio rectangular, pueden ser widgets o diseños, y tienen propiedades, como identificadores, posición, margen, padding, etcétera.

Compose utiliza otro enfoque. En lugar de elementos View, se definen funciones que admiten composición y que emiten la IU. Los elementos que admiten composición no tienen un ID ni descripción de contenido. Entonces, ¿cómo se hace, por ejemplo, para hacer clic en un botón en una prueba de IU? En este documento se explican los enfoques equivalentes para Compose.

Semántica

Las pruebas de IU en Compose usan la semántica para interactuar con la jerarquía de la IU. La semántica, como su nombre lo indica, le da significado a una parte de la IU. En este contexto, una "parte de la IU" (o elemento) puede hacer referencia a todo, desde un único elemento que admite composición hasta una pantalla completa. Se elabora el árbol semántico a lo largo de la jerarquía de la IU y la describe.

Diagrama que muestra un diseño de IU típico y la forma en que este se asignaría al árbol semántico correspondiente

Figura 1: La jerarquía de la IU y su árbol semántico

El framework semántico se usa principalmente para fines de accesibilidad, por lo que las pruebas aprovechan la información que muestra la semántica sobre la jerarquía de la IU. Los desarrolladores deciden qué exponer y en qué medida.

Botón que contiene un gráfico y texto

Figura 2: Botón típico que contiene un ícono y texto

Por ejemplo, en un botón como este con un ícono y un elemento de texto, el árbol semántico predeterminado solo contiene la etiqueta de texto "Me gusta". Esto se debe a que algunos elementos que admiten composición, como Text, ya exponen algunas propiedades al árbol semántico. Puedes agregar propiedades al árbol a través de un Modifier.

Text(modifier = Modifier.semantics { text = text } )

Puedes anular el resultado semántico predeterminado. Por ejemplo, en este caso, podrías hacer que el botón emita un nodo con la etiqueta de texto "Botón de Me gusta":

MyButton(modifier = Modifier.semantics { text = "Like button" } )

Configuración

En esta sección se describe cómo configurar el módulo para probar el código de Compose.

Primero, agrega las siguientes dependencias al archivo build.gradle del módulo que contiene las pruebas de tu IU:

androidTestImplementation("androidx.ui:ui-test:$compose_version")

En este módulo, se incluye una ComposeTestRule y una implementación para Android llamada AndroidComposeTestRule. Con esta regla, puedes establecer el contenido de Compose o acceder a la actividad. La prueba típica de IU para Compose se ve de la siguiente manera:

// file: app/src/androidTest/java/com/package/MyComposeTest.kt

class MyComposeTest {

    @get:Rule
    val composeTestRule = createAndroidComposeRule<MyActivity>()
    // createComposeRule() if you don't need access to the activityTestRule

    @Test
    fun MyTest() {
        // Start the app
        composeTestRule.setContent {
            MyAppTheme {
                MainScreen(uiState = exampleUiState, ...)
            }
        }

        onNodeWithText("Continue").performClick()

        onNodeWithText("Welcome").assertIsDisplayed()
    }
}

Si usas createComposeRule, deberás agregar ComponentActivity a tu manifiesto de Android. Te recomendamos que lo hagas en app/src/debug/AndroidManifest.xml:

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application>
        <activity android:name="androidx.activity.ComponentActivity" />
    </application>
</manifest>

API de prueba

Si usaste Espresso, las API de prueba de Compose te resultarán familiares.

Compose utiliza comparadores SemanticsMatcher para hacer referencia a uno o más nodos en el árbol semántico. Una vez que vincules uno o más nodos, podrás realizar acciones sobre ellos o hacer aserciones.

Buscadores

Seleccionar un solo nodo

onNode(<<SemanticsMatcher>>, useUnmergedTree = false): SemanticsNodeInteraction
// Example
onNode(hasText("Button")) // Equivalent to onNodeWithText("Button")

Seleccionar varios nodos

onAllNodes(<<SemanticsMatcher>>): SemanticsNodeInteractionCollection
// Example
onAllNodes(hasText("Button")) // Equivalent to onAllNodesWithText("Button")

Cómo usar el árbol separado

Algunos nodos fusionan la información semántica de sus elementos secundarios. Por ejemplo, un botón con dos elementos de texto fusiona sus etiquetas:

Button { Text("Hello"), Text("World") }

En una prueba, podemos usar printToLog() para mostrar el árbol semántico:

onRoot().printToLog("TAG")

Este código muestra el siguiente resultado:

Node #1 at (...)px
 |-Node #2 at (...)px
   OnClick = '...'
   Text = Hello World
   MergeDescendants = 'true'

Si necesitas vincular un nodo de lo que sería el árbol separado, puedes configurar useUnmergedTree como true:

onRoot(useUnmergedTree = true).printToLog("TAG")

Este código muestra el siguiente resultado:

Node #1 at (...)px
 |-Node #2 at (...)px
   OnClick = '...'
   MergeDescendants = 'true'
    |-Node #3 at (...)px
    | Text = 'Hello'
    |-Node #5 at (83.0, 86.0, 191.0, 135.0)px
      Text = 'World'

El parámetro useUnmergedTree está disponible en todos los buscadores. Por ejemplo, aquí se usa en un buscador onNodeWithText.

onNodeWithText("World", useUnmergedTree = true).assertIsDisplayed()

Aserciones

Para verificar las aserciones, llama a assert() en el objeto SemanticsNodeInteraction que muestra un buscador con uno o varios comparadores:

// Single matcher:
onNode(...).assert(hasText("Button")) // hasText is a SemanticsMatcher
// Multiple matchers can use and / or
onNode(...).assert(hasText("Button") or hasText("Button2"))

También existen funciones para verificar aserciones en una colección de nodos:

// Check number of matched nodes
onAllNodesWithLabel("Beatle").assertCountEquals(4)
// At least one matches
onAllNodesWithLabel("Beatle").assertAny(hasTestTag("Drummer"))
// All of them match
onAllNodesWithLabel("Beatle").assertAll(assertHasClickAction())

Acciones

Para insertar una acción en un nodo, llama a una función perform…():

onNode(...).performClick()

Estos son algunos ejemplos de acciones:

performClick(),
performSemanticsAction(),
performKeyPress(),
performGesture { swipeLeft() }

Comparadores

En esta sección se describen algunos de los comparadores disponibles para probar tu código de Compose.

Comparadores jerárquicos

Los comparadores jerárquicos te permiten subir o bajar por el árbol semántico, y realizar vinculaciones simples.

fun hasParent(matcher: SemanticsMatcher): SemanticsMatcher
fun hasAnySibling(matcher: SemanticsMatcher): SemanticsMatcher
fun hasAnyAncestor(matcher: SemanticsMatcher): SemanticsMatcher
fun hasAnyDescendant(matcher: SemanticsMatcher):  SemanticsMatcher

Estos son algunos ejemplos de cómo se usan esos comparadores:

onNode(hasParent(hasText("Button"))
    .assertIsVisible()

Selectores

Otra forma de crear pruebas es usar selectores, que pueden hacer que algunas pruebas sean más legibles.

onNode(hasTag("Players"))
    .onChildren()
    .filter(hasClickAction())
    .assertCountEquals(4)
    .onFirst()
    .assert(hasText("John"))

Patrones comunes

En esta sección se describen algunos enfoques comunes que verás en las pruebas de Compose.

Cómo realizar pruebas de forma aislada

ComposeTestRule te permite iniciar una actividad que muestre elementos que admiten composición: tu aplicación completa, una sola pantalla o un widget. También se recomienda comprobar que los elementos que admiten composición estén bien encapsulados y funcionen de manera independiente para poder probar las IU de forma más fácil y precisa.

Eso no significa que solo deberías crear pruebas de IU por unidad. También son muy importantes las pruebas de IU que abarcan partes más grandes de la IU.

Cómo simular cambios de configuración

La configuración en Compose se realiza mediante ambientes. Los ambientes describen las características del dispositivo en el que se ejecuta tu app. Cada vez que cambia la configuración, el ambiente recompone la parte pertinente del árbol. La configuración contiene información como el tamaño y la orientación de la pantalla, el modo nocturno, etcétera. En lugar de pedir al dispositivo que cambie esos parámetros, es mucho más rápido y confiable simular el cambio de configuración en Compose:

class MyTest() {

    private val themeIsDark = MutableStateFlow(false)

    @Before
    fun setUp() {
        composeTestRule.setContent {
            JetchatTheme(
              isDarkTheme = themeIsDark.collectAsState(false).value
            ) {
                MainScreen()
            }
        }
    }

    @Test
    fun changeTheme_scrollIsPersisted() {
        findByLabel("Continue").doClick()

        // Set theme to dark
        themeIsDark.value = true

        // Check that we're still on the same page
        findByLabel("Welcome").assertIsDisplayed()
    }
}

Depuración

La principal manera de resolver problemas en tus pruebas es observar el árbol semántico. Para mostrar el árbol, llama a findRoot().printToLog() en cualquier punto de la prueba. Esta función muestra un registro como este:

    Node #1 at (...)px
     |-Node #2 at (...)px
       OnClick = '...'
       MergeDescendants = 'true'
        |-Node #3 at (...)px
        | Text = 'Hi'
        |-Node #5 at (83.0, 86.0, 191.0, 135.0)px
          Text = 'There'

Esos registros contienen información valiosa para el seguimiento de errores.

Cómo inhabilitar animaciones

A menos que tu prueba verifique el comportamiento correcto de las animaciones, es probable que quieras inhabilitarlas para que la prueba se ejecute lo más rápido posible. Como es posible que las animaciones ralenticen considerablemente la prueba, AndroidComposeTestRule brinda un parámetro para inhabilitarlas a todas:

@get:Rule
val composeTestRule =
  AndroidComposeTestRule<MyActivity>(disableTransitions = true)

Ya no es necesario configurar las opciones de transición y animación del sistema en Opciones para desarrolladores.