Las versiones v2 de las APIs de prueba de Compose (createComposeRule, createAndroidComposeRule, runComposeUiTest, runAndroidComposeUiTest, etc.) ya están disponibles para mejorar el control de la ejecución de corrutinas. Esta actualización no duplica toda la plataforma de la API,
solo se actualizaron las APIs que establecen el entorno de pruebas.
Las APIs v1 están obsoletas y se recomienda migrar a las APIs nuevas. La migración verifica que tus pruebas se alineen con el comportamiento estándar de las corrutinas y evita problemas de compatibilidad futuros. Para obtener una lista de las APIs de v1 obsoletas, consulta Asignaciones de APIs.
Mientras que las APIs de la versión 1 dependían de UnconfinedTestDispatcher, las APIs de la versión 2 usan
StandardTestDispatcher de forma predeterminada para la composición en ejecución. Este cambio
alinea el comportamiento de las pruebas de Compose con las APIs de runTest estándar y proporciona
un control explícito sobre el orden de ejecución de las corrutinas.
Configura el entorno de pruebas
Las APIs de Compose test v2 usan ComposeUiTestConfig para personalizar el entorno de pruebas. Las APIs que crean funciones de configuración para pruebas, como createComposeRule, runComposeUiTest y otras APIs relacionadas, aceptan ComposeUiTestConfig. Este objeto de configuración consolida
las APIs relacionadas con el entorno, como effectContext, runTestContext
y testTimeout, en un solo objeto.
El modelo de configuración también administra inputMode. Las APIs de Compose Test v2 aplican InputMode.Touch de forma predeterminada al comienzo de cada prueba para garantizar el determinismo y evitar que el estado del modo de entrada se filtre entre las pruebas.
ComposeUiTestConfig forma parte de las APIs de pruebas de Compose v2, que usan
StandardTestDispatcher de forma predeterminada. Si tus pruebas usan las APIs de v1,
consulta Migra a las APIs de prueba de v2 antes de adoptar
ComposeUiTestConfig.
Migra a ComposeUiTestConfig
Dentro de las sobrecargas para crear funciones de configuración en las pruebas, se dejaron de usar varias sobrecargas que aceptan parámetros de configuración individuales, como effectContext, runTestContext o testTimeout. Actualiza tus pruebas para usar
ComposeUiTestConfig, como se muestra en el siguiente ejemplo:
val testConfig = ComposeUiTestConfig( effectContext = EmptyCoroutineContext, runTestContext = EmptyCoroutineContext, testTimeout = 30.seconds ) @get:Rule val rule = createComposeRule(config = testConfig) // OR runComposeUiTest(config = testConfig) {}
Modo de entrada predeterminado
Las pruebas pueden fallar durante la migración si dependen de modos de entrada que no sean táctiles y que se hayan configurado a través de las APIs de instrumentación antes de que comience la prueba. Dentro de las funciones de configuración de las pruebas, el sistema aplica InputMode.Touch de forma predeterminada al comienzo de cada prueba para lograr un mayor determinismo y evitar la filtración de estado, lo que anula el estado del dispositivo ambiental y la configuración previa a la prueba.
Para resolver este problema, especifica el modo de entrada requerido en ComposeUiTestConfig:
class FocusTest { @get:Rule val rule = createComposeRule( config = ComposeUiTestConfig(inputMode = InputMode.Keyboard) ) @Test fun testFocus() {} }
Para configurar el modo de entrada de casos de prueba individuales en lugar de toda la clase de prueba, pasa ComposeUiTestConfig a runComposeUiTest:
class FocusTest { @Test fun testTouchMode() = runComposeUiTest { // Runs with the default InputMode.Touch } @Test fun testKeyboardMode() = runComposeUiTest( ComposeUiTestConfig(inputMode = InputMode.Keyboard) ) { // Runs with InputMode.Keyboard } }
Para conocer otros problemas y soluciones de migración, consulta Fallas comunes y cómo corregirlas.
Migra a las APIs de prueba de la versión 2
Cuando actualices a las APIs de la versión 2, por lo general, podrás usar Buscar y reemplazar para actualizar las importaciones de paquetes y adoptar los nuevos cambios del despachador.
Como alternativa, pídele a Gemini que realice una migración a la versión 2 de las APIs de pruebas de Compose con la siguiente instrucción:
Instrucción de IA de
Migra de las APIs de prueba v1 a las APIs de prueba v2
Esta instrucción usará esta guía para migrar a las APIs de prueba de la versión 2.
Migrate to Compose testing v2 APIs using the official
migration guide.Usa la siguiente tabla para asignar las APIs de la versión 1 obsoletas a sus reemplazos de la versión 2:
Obsoleta (v1) |
Reemplazo (v2) |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Retrocompatibilidad y excepciones
Las APIs de la versión 1 existentes ahora están obsoletas, pero siguen usando
UnconfinedTestDispatcher para mantener el comportamiento existente y evitar cambios
rotundos.
La siguiente es la única excepción en la que cambió el comportamiento predeterminado:
El despachador de pruebas predeterminado que se usa para ejecutar la composición en la clase AndroidComposeUiTestEnvironment cambió de UnconfinedTestDispatcher a StandardTestDispatcher. Esto afecta los casos en los que creas una instancia con el constructor o la subclase AndroidComposeUiTestEnvironment, y llamas a ese constructor.
Cambio clave: Impacto en la ejecución de corrutinas
La diferencia principal entre la versión 1 y la versión 2 de las APIs es la forma en que se despachan las corrutinas:
- APIs v1 (
UnconfinedTestDispatcher): Cuando se iniciaba una corrutina, se ejecutaba de inmediato en el subproceso actual y, a menudo, finalizaba antes de que se ejecutara la siguiente línea de código de prueba. A diferencia del comportamiento de producción, esta ejecución inmediata podría enmascarar de forma inadvertida problemas de sincronización reales o condiciones de carrera que ocurrirían en una aplicación en vivo. - APIs de la versión 2 (
StandardTestDispatcher): Cuando se inicia una corrutina, se pone en cola y no se ejecuta hasta que la prueba avanza de forma explícita el reloj virtual. Las APIs de prueba estándar de Compose (comowaitForIdle()) ya controlan esta sincronización, por lo que la mayoría de las pruebas que dependen de estas APIs estándar deberían seguir funcionando sin cambios.
Fallas comunes y cómo corregirlas
Si tus pruebas fallan después de actualizar a la versión 2, es probable que muestren el siguiente patrón:
- Falla: Inicias una tarea (por ejemplo, un ViewModel carga datos), pero tu aserción falla de inmediato porque los datos aún están en un estado de "Cargando".
- Causa: Con las APIs de la versión 2, las corrutinas se ponen en cola en lugar de ejecutarse de inmediato. La tarea se puso en cola, pero nunca se ejecutó antes de que se verificara el resultado.
- Solución: Avanza el tiempo de forma explícita. Debes indicarle de forma explícita al despachador de la versión 2 cuándo ejecutar el trabajo.
Enfoque anterior
En la versión 1, la tarea se inició y finalizó de inmediato. En la versión 2, el siguiente código
falla porque loadData() aún no se ejecutó.
// In v1, this launched and finished immediately.
viewModel.loadData()
// In v2, this fails because loadData() hasn't actually run yet!
assertEquals(Success, viewModel.state.value)
Enfoque recomendado
Usa waitForIdle o runOnIdle para ejecutar tareas en cola antes de
realizar aserciones.
Opción 1: El uso de waitForIdle hace avanzar el reloj hasta que la IU esté inactiva, lo que verifica que se haya ejecutado la corrutina.
viewModel.loadData()
// Explicitly run all queued tasks
composeTestRule.waitForIdle()
assertEquals(Success, viewModel.state.value)
Opción 2: El uso de runOnIdle ejecuta el bloque de código en el subproceso de IU después de que la IU se haya inactiva.
viewModel.loadData()
// Run the assertion after the UI is idle
composeTestRule.runOnIdle {
assertEquals(Success, viewModel.state.value)
}
Sincronización manual
En situaciones que implican una sincronización manual, como cuando se inhabilita el avance automático, el lanzamiento de una corrutina no genera una ejecución inmediata porque el reloj de prueba está en pausa. Para ejecutar corrutinas en la cola sin
avanzar el reloj virtual, usa la API de runCurrent(). Esto ejecuta las tareas
programadas para la hora virtual actual.
composeTestRule.mainClock.scheduler.runCurrent()
A diferencia de waitForIdle(), que avanza el reloj de prueba hasta que se estabiliza la IU, runCurrent() ejecuta las tareas pendientes mientras mantiene el tiempo virtual actual. Este comportamiento permite verificar los estados intermedios que, de lo contrario, se omitirían si el reloj avanzara a un estado inactivo.
Se expone el programador de pruebas subyacente que se usa en el entorno de pruebas. Este programador se puede usar junto con la API de runTest de Kotlin para sincronizar el reloj de prueba.
Migra a runComposeUiTest
Si usas las APIs de prueba de Compose junto con la API de runTest de Kotlin, te recomendamos que cambies a runComposeUiTest.
Enfoque anterior
Si usas createComposeRule junto con runTest, se crean dos relojes independientes: uno para Compose y otro para el alcance de la corrutina de prueba. Esta configuración puede obligarte a sincronizar manualmente el programador de pruebas.
@get:Rule val composeTestRule = createComposeRule() @Test fun testWithCoroutines() { composeTestRule.setContent { var status by remember { mutableStateOf("Loading...") } LaunchedEffect(Unit) { delay(1000) status = "Done!" } Text(text = status) } // NOT RECOMMENDED // Fails: runTest creates a new, separate scheduler. // Advancing time here does NOT advance the compose clock. // To fix this without migrating, you would need to share the scheduler // by passing 'composeTestRule.mainClock.scheduler' to runTest. runTest { composeTestRule.onNodeWithText("Loading...").assertIsDisplayed() advanceTimeBy(1000) composeTestRule.onNodeWithText("Done!").assertIsDisplayed() } }
Enfoque recomendado
La API de runComposeUiTest ejecuta automáticamente tu bloque de prueba dentro de su propio
alcance de runTest. El reloj de prueba se sincroniza con el entorno de Compose, por lo que ya no necesitas administrar el programador de forma manual.
@Test fun testWithCoroutines() = runComposeUiTest { setContent { var status by remember { mutableStateOf("Loading...") } LaunchedEffect(Unit) { delay(1000) status = "Done!" } Text(text = status) } onNodeWithText("Loading...").assertIsDisplayed() mainClock.advanceTimeBy(1000 + 16 /* Frame buffer */) onNodeWithText("Done!").assertIsDisplayed() } }