Sincroniza tus pruebas

Las pruebas de Compose se sincronizan de forma predeterminada con tu IU. Cuando llamas a una aserción o una acción con ComposeTestRule, la prueba se sincroniza antes y se espera hasta que el árbol de IU esté inactivo.

Normalmente, no es necesario que realices ninguna acción. Sin embargo, hay algunos casos extremos que debes conocer.

Cuando se sincroniza una prueba, tu app de Compose está avanzada a tiempo con un reloj virtual. Eso significa que las pruebas de Compose no se ejecutan en tiempo real, por lo que pueden pasar tan rápido como sea posible.

Sin embargo, si no usas los métodos que sincronizan tus pruebas, no se producirá una recomposición, y la IU se pausará.

@Test
fun counterTest() {
    val myCounter = mutableStateOf(0) // State that can cause recompositions.
    var lastSeenValue = 0 // Used to track recompositions.
    composeTestRule.setContent {
        Text(myCounter.value.toString())
        lastSeenValue = myCounter.value
    }
    myCounter.value = 1 // The state changes, but there is no recomposition.

    // Fails because nothing triggered a recomposition.
    assertTrue(lastSeenValue == 1)

    // Passes because the assertion triggers recomposition.
    composeTestRule.onNodeWithText("1").assertExists()
}

Ten en cuenta que este requisito solo se aplica a las jerarquías de Compose y no al resto de la app.

Cómo inhabilitar la sincronización automática

Cuando llamas a una aserción o una acción a través de ComposeTestRule, como assertExists(), tu prueba se sincroniza con la IU de Compose. En algunos casos, puedes detener esta sincronización y controlar tú mismo el reloj. Por ejemplo, puedes controlar el tiempo para tomar capturas de pantalla precisas de una animación en un punto en el que la IU esté ocupada. Para inhabilitar la sincronización automática, configura la propiedad autoAdvance de mainClock como false:

composeTestRule.mainClock.autoAdvance = false

Normalmente, avanzarás el tiempo tú mismo. Puedes avanzar de a un fotograma con advanceTimeByFrame() o por una duración específica con advanceTimeBy():

composeTestRule.mainClock.advanceTimeByFrame()
composeTestRule.mainClock.advanceTimeBy(milliseconds)

Recursos inactivos

Compose puede sincronizar pruebas y la IU para que todas las acciones y aserciones se realicen en estado inactivo, a la espera del reloj o avanzándolo según sea necesario. Sin embargo, algunas operaciones asíncronas cuyos resultados afectan el estado de la IU se pueden ejecutar en segundo plano mientras la prueba no las tiene en cuenta.

Crea y registra estos recursos inactivos en tu prueba para que se los tenga en cuenta cuando determines si la app en cuestión está ocupada o inactiva. No tienes que realizar ninguna acción, a menos que necesites registrar recursos inactivos adicionales, por ejemplo, si ejecutas un trabajo en segundo plano que no se sincroniza con Espresso ni Compose.

Esta API es muy similar a los recursos inactivos de Espresso para indicar si el sujeto en cuestión está inactivo o ocupado. Usa la regla de prueba de Compose para registrar la implementación de IdlingResource.

composeTestRule.registerIdlingResource(idlingResource)
composeTestRule.unregisterIdlingResource(idlingResource)

Sincronización manual

En algunos casos, debes sincronizar la IU de Compose con otras partes de la prueba o la app que estás probando.

La función waitForIdle() espera a que Compose esté inactivo, pero la función depende de la propiedad autoAdvance:

composeTestRule.mainClock.autoAdvance = true // Default
composeTestRule.waitForIdle() // Advances the clock until Compose is idle.

composeTestRule.mainClock.autoAdvance = false
composeTestRule.waitForIdle() // Only waits for idling resources to become idle.

Ten en cuenta que, en ambos casos, waitForIdle() también espera las pasadas de diseño y dibujo pendientes.

Además, puedes adelantar el reloj hasta que se cumpla una condición determinada con advanceTimeUntil().

composeTestRule.mainClock.advanceTimeUntil(timeoutMs) { condition }

Ten en cuenta que la condición determinada debe comprobar el estado que puede verse afectado por este reloj (solo funciona con el estado de Compose).

Optimiza las pruebas de animación

Cuando pruebas animaciones de alta fidelidad, a menudo necesitas inhabilitar el avance automático y recorrer los fotogramas de forma manual para confirmar los estados intermedios de la IU. Para estos bucles específicos fotograma por fotograma, usa el runWithoutImplicitWait método para ejecutar tus aserciones. Las consultas de nodos estándar (como onNodeWithTag o fetchSemanticsNode) activan sincronizaciones implícitas que son redundantes cuando controlas el reloj de forma manual, por lo que omitirlas acelera significativamente los tiempos de ejecución de las pruebas.

lineamientos de uso

  • Administración manual del reloj: Usa esta API cuando mainClock.autoAdvance esté configurado como false y la IU se encuentre en un estado estable y conocido para el fotograma actual.
  • Ejecución del subproceso de IU: Para garantizar la estabilidad del árbol de la IU, llama a runWithoutImplicitWait en el subproceso de IU, como con runOnUiThread. Si lo ejecutas fuera del subproceso de IU, la prueba estará expuesta a condiciones de carrera y lecturas de estado obsoletas.
  • Aserciones de solo lectura: El bloque debe contener estrictamente aserciones de solo lectura. Cualquier acción que modifique el estado debe realizarse fuera de este bloque.

Ejemplo

@Test
fun runWithoutImplicitWaitSample() = runComposeUiTest {
    setContent { MainScreen() }
    mainClock.autoAdvance = false

    // Trigger an animation
    onNodeWithText("Start Animation").performClick()

    // Step through the animation frame-by-frame
    while (hasPendingWork()) {
        mainClock.advanceTimeByFrame()
        waitForIdle()
        runOnUiThread {
            // Suppress implicit synchronization inside this block to avoid redundant
            // waits on each node query, making the frame assertions execute much faster.
            runWithoutImplicitWait {
                val box1 = onNodeWithTag("Box1").fetchSemanticsNode()
                val box2 = onNodeWithTag("Box2").fetchSemanticsNode()
                val box3 = onNodeWithTag("Box3").fetchSemanticsNode()

                // Assert the exact intermediate state of all three properties for this frame
                assert(box1.boundsInRoot.right <= box2.boundsInRoot.left)
                assert(box2.boundsInRoot.right <= box3.boundsInRoot.left)
            }
        }
    }
}

Sincronización del subproceso principal

Las pruebas de Compose ahora admiten la sincronización del subproceso principal, lo que te permite llamar de forma segura a waitForIdle y, por extensión, a las acciones y aserciones de la IU de Compose, directamente desde el subproceso principal.

Anteriormente, las pruebas de Compose aplicaban estrictamente un modelo de dos subprocesos: la ejecución de pruebas se realizaba en un subproceso de prueba en segundo plano, mientras que las actualizaciones de la IU se realizaban en el subproceso principal. Llamar a métodos de sincronización como waitForIdle o runOnIdle desde el subproceso principal (por ejemplo, dentro de un bloque runOnUiThread) arrojaría una IllegalStateException porque el framework aplicaba verificaciones estrictas de subprocesos para evitar la sincronización del subproceso principal.

Con la sincronización del subproceso principal habilitada, el framework de pruebas de Compose ahora puede avanzar el reloj y procesar el trabajo pendiente, incluso cuando se realizan llamadas de bloqueo en el subproceso principal.

Cuándo usar la sincronización del subproceso principal

Si bien mantener las pruebas en el subproceso en segundo plano sigue siendo el estándar para las pruebas puras de Compose, la sincronización del subproceso principal es muy ventajosa en algunos casos específicos:

  • Interoperabilidad compleja de View: Cuando se prueban IUs híbridas que contienen Compose y Views heredadas de Android, la manipulación de Views suele requerir la ejecución en el subproceso principal. Ahora puedes interactuar con Views y realizar aserciones en nodos de Compose de forma secuencial sin cambiar constantemente los contextos de subprocesos.
  • Mutaciones de estado síncronas: Si tu arquitectura se basa en titulares de estado estrictamente vinculados al subproceso principal, ahora puedes mutar el estado y esperar de inmediato a que se establezca la IU de Compose sin salir del subproceso principal.
  • Ejecutores de pruebas personalizados: Si compilas una infraestructura de pruebas personalizada o utilizas entornos en los que el ejecutor de pruebas se ejecuta de forma inherente en el subproceso principal, las pruebas de Compose ahora se ejecutan de forma limpia sin requerir la delegación de subprocesos en segundo plano.

Ejemplo

Históricamente, debido a que la sincronización estaba estrictamente prohibida en el subproceso principal, los desarrolladores tenían que ir y venir entre el subproceso del ejecutor de pruebas en segundo plano y el subproceso de la IU, lo que generaba pruebas inconexas:

@Test
fun testBidirectionalInteropUIUpdates_old() {
    val scenario = launchFragmentInContainer<InteropFragment>()
    composeTestRule.waitForIdle()
    scenario.onFragment { fragment ->
        fragment.legacyButton.performClick()
    }
    // Jump to Test Thread to verify state settles inside compose
    composeTestRule.waitForIdle()
    composeTestRule.onNodeWithText("Legacy Clicks: 1").assertIsDisplayed()
    composeTestRule.onNodeWithText("Increment Legacy TextView").performClick()
    composeTestRule.waitForIdle()
    // Jump back to Main Thread to verify target view state settles
    scenario.onFragment { fragment ->
        assert(fragment.legacyTextView.text.toString() == "Compose Clicks: 1")
    }
}

Con la sincronización del subproceso principal habilitada, las aserciones para las jerarquías de Compose y View se pueden ejecutar en el mismo bloque:

@Test
fun testBidirectionalInteropUIUpdates_new() {
    val scenario = launchFragmentInContainer<InteropFragment>()
    composeTestRule.waitForIdle()
    scenario.onFragment { fragment ->
        fragment.legacyButton.performClick()
        composeTestRule.waitForIdle()
        composeTestRule.onNodeWithText("Legacy Clicks: 1").assertIsDisplayed()
        composeTestRule.onNodeWithText("Increment Legacy TextView").performClick()
        composeTestRule.waitForIdle()
        assert(fragment.legacyTextView.text.toString() == "Compose Clicks: 1")
    }
}

Espera las condiciones

Cualquier condición que dependa de un trabajo externo, como la carga de datos o la medición o el dibujo de Android (es decir, medición o dibujo externo a Compose), debe usar un concepto más general, como waitUntil():

composeTestRule.waitUntil(timeoutMs) { condition }

También puedes usar cualquiera de los waitUntil asistentes:

composeTestRule.waitUntilAtLeastOneExists(matcher, timeoutMs)

composeTestRule.waitUntilDoesNotExist(matcher, timeoutMs)

composeTestRule.waitUntilExactlyOneExists(matcher, timeoutMs)

composeTestRule.waitUntilNodeCount(matcher, count, timeoutMs)

Recursos adicionales

  • Cómo probar apps en Android: La página de destino principal de las pruebas de Android proporciona una visión más amplia de los aspectos básicos y las técnicas de prueba.
  • Aspectos básicos de las pruebas: Obtén más información sobre los conceptos básicos detrás de las pruebas de una app para Android.
  • Pruebas locales: Puedes ejecutar algunas pruebas de forma local en tu propia estación de trabajo.
  • Pruebas instrumentadas: También se recomienda ejecutar pruebas instrumentadas. Es decir, pruebas que se ejecutan directamente integrado en el dispositivo.
  • Integración continua: La integración continua te permite integrar tus pruebas en tu canalización de implementación.
  • Cómo probar diferentes tamaños de pantalla: Con tantos dispositivos disponibles para los usuarios, debes probar diferentes tamaños de pantalla.
  • Espresso: Si bien está diseñada para IUs basadas en View, el conocimiento de Espresso puede ser útil para algunos aspectos de las pruebas de Compose.