Pruebas de navegación entre pantallas con XCUITest

Resumen

Probar la navegación entre pantallas en iOS requiere algo más que validar elementos estáticos. Cuando trabajas con XCUITest y flujos que abren sheets o navegan a vistas de detalle, necesitas manejar transiciones, predicados y tiempos de espera. Esta guía te muestra cómo escribir pruebas de UI para flujos dinámicos en una app iOS, ideal para desarrolladores que ya dominan los tests básicos y quieren llevar su cobertura al siguiente nivel.

¿Cómo pruebo la apertura de un formulario modal en XCUITest?

El primer flujo a cubrir es el de levantar un formulario para agregar un nuevo registro. Dentro de HomeViewUITest se crea una función llamada testAddRecord que valida la existencia del botón antes de interactuar con él [00:18].

El patrón es simple pero estricto:

  • Validar que el botón existe con XCTAssertTrue(addRecordButton.exists).
  • Ejecutar la acción con addRecordButton.tap().
  • Verificar que un elemento de la siguiente pantalla aparezca tras la transición.

Aquí aparece el primer detalle técnico importante. Como hay una transición visual entre el tap y la aparición del sheet, no puedes usar .exists directamente. La pantalla todavía no terminó de renderizarse cuando tu test ya está preguntando si existe el elemento.

¿Qué es waitForExistence en XCUITest? Es un método que espera hasta que un elemento aparezca en la UI dentro de un timeout definido. Si el elemento no aparece en ese tiempo, el test falla. Es la forma correcta de manejar transiciones asíncronas entre pantallas.

Por eso se usa newRecordSheet.waitForExistence(timeout: 3), donde newRecordSheet apunta al staticTexts["formRecordTitle"] de la siguiente pantalla [01:20]. Tres segundos es un margen amplio que evita falsos negativos por lentitud del simulador.

¿Cómo busco elementos dinámicos con predicados en XCUITest?

El segundo flujo es más interesante: navegar al detalle de un registro existente. El reto es que los registros se generan dinámicamente, así que no puedes usar un identificador fijo. Aquí entra el concepto de mock data, configurado previamente para garantizar que la vista siempre tenga registros durante las pruebas de UI [02:30].

La solución es construir una query con NSPredicate. En lugar de buscar un identificador exacto, buscas cualquier botón cuyo identificador empiece con un patrón conocido:

swift let firstRecord = app.buttons.matching( NSPredicate(format: "identifier BEGINSWITH 'record_'") ).firstMatch

La función matching filtra todos los botones que cumplen el predicado y devuelve una colección. Con .firstMatch obtienes el primer elemento que coincide, sin importar su ID específico [03:15].

¿Para qué sirve firstMatch en pruebas de UI? Devuelve el primer elemento que coincide con un predicado o consulta. Es útil cuando trabajas con listas dinámicas y solo necesitas interactuar con un elemento representativo, no con uno específico.

El flujo completo del test queda así:

  1. Construir el predicado con BEGINSWITH 'record_'.
  2. Obtener el primer match y validar su existencia con XCTAssertTrue.
  3. Hacer tap sobre el registro.
  4. Validar que un elemento de la vista de detalle aparezca, por ejemplo el botón de eliminar.

¿Por qué necesito waitForExistence al navegar entre pantallas?

Al revisar el código de detalle, aparece el mismo error sutil que en el primer test. Validar la vista de detalle con .exists falla porque la transición de navegación toma tiempo. La corrección consiste en reemplazar detailView.exists por detailView.waitForExistence(timeout: 3) [04:40].

La regla práctica es clara: cualquier validación posterior a una transición de pantalla debe usar waitForExistence. Esto incluye:

  • Apertura de sheets o modales.
  • Navegación push a vistas de detalle.
  • Cambios de tab que recargan contenido.
  • Animaciones que retrasan el render de elementos.

Usar .exists en estos casos genera tests inestables, los famosos flaky tests que pasan en local y fallan en CI sin razón aparente.

Habilidades y conceptos que se trabajan en esta clase

A lo largo del ejercicio se ponen en práctica varias habilidades clave del UI testing en iOS:

  • XCTAssertTrue: aserción que valida que una condición booleana sea verdadera. Es la base de las verificaciones en cada paso del test [00:45].
  • NSPredicate con BEGINSWITH: técnica para construir consultas flexibles sobre identificadores dinámicos, útil cuando los IDs incluyen sufijos variables como un UUID [03:00].
  • firstMatch: propiedad que extrae el primer elemento de una colección filtrada por matching, ideal para listas generadas en tiempo de ejecución [03:20].
  • waitForExistence(timeout:): método que espera hasta que un elemento exista dentro de un tiempo máximo, fundamental para flujos con transiciones [01:25].
  • Mock data en UI testing: estrategia de inyectar datos de prueba para garantizar que la UI tenga contenido predecible durante los tests [02:35].

Con estos cuatro pilares cubres la mayoría de escenarios de navegación en una app iOS estándar. El reporte final del test runner muestra todos los casos en verde: apertura del sheet, selección de filtro, validación de elementos en la UI y navegación al detalle [05:30].

Quedan vistas pendientes por cubrir en la app. Implementa los tests faltantes siguiendo este mismo patrón y cuéntame en los comentarios qué predicados usaste y cómo manejaste las transiciones más complicadas.