UITests en SwiftUI y Xcode del curso de Testing
UITests en SwiftUI y Xcode del curso de Testing

UITests en SwiftUI (Parte 8)

Explora nuestra guía completa sobre UITests en Xcode para principiantes. Aprende cómo realizar pruebas de interfaz de usuario en aplicaciones iOS y asegura una experiencia de usuario impecable con nuestros consejos y trucos expertos

SwiftBeta

Tabla de contenido


👇 SÍGUEME PARA APRENDER SWIFTUI, SWIFT, XCODE, etc 👇

Hoy en SwiftBeta vamos a aprender a crear otro tipo de tests, hasta ahora hemos visto los tests unitarios, de integración y snapshot. Y hoy toca hablar sobre los UITests. ¿Te acuerdas de la pirámide sobre testing que te comentaba hace unos videos? Dentro de esa pirámide vamos a colocar los UITest en la parte de arriba del todo, aquí arriba encontramos los E2E, los tests de Extremo a Extremo. La ventaja de estos tests es que probamos nuestra app interactuando con nuestra UI, grabando todos los pasos para luego poderlos lanzar en nuestros tests automatizados ¿Cómo? no te preocupes que va a quedar muy claro

La ventaja de los UITest es testear flujos de nuestra app, por ejemplo, el login, registro, entrar a nuestra aplicación, y luego navegar para ver si la lógica se comporta tal y como esperamos, etc.

En el video de hoy vamos a crear 2 UITests, uno para crear una nota y ver que se ha creado correctamente, y otro tests para borrar la nota que acabamos de crear. En este caso usaremos la base de datos de la aplicación, pero es recomendable usar lo que hemos visto hasta ahora de inyectar una Base de datos en memoria cuando trabajemos con los tests, sino sabes de lo que te hablo, mírate la serie sobre testing porque lo hemos explicado mucho y lo hemos practicado.

Vamos a usar directamente el simulador, y a medida que nosotros interactuemos con nuestra aplicación en el simulador, verás que el van apareciendo instrucciones dentro de nuestro test que se van creando automáticamente. Estamos "grabando todos los pasos que queremos seguir.

Los UITest son tests costosos en el sentido de que son más lentos que los Tests Unitarios o de Integración (por eso están arriba del todo de la pirámide del Testing, por que deberíamos de tener una menor suite de tests de este tipo en nuestra app). Cuando ejecutemos el test puedes pensar que va relativamente rápido, pero al final, en tu suite de tests acabarás teniendo miles de tests, y te interesa que los UITest sean pocos pero que cubran un buen scope de tu app. Estos tipos de test donde es necesario el simulador, pueden aumentar los costes en tu CI si no los optimizas lo suficiente.

Vamos a empezar, y vamos a crear nuestro primero UITest

Aprende a crear UITests usando Xcode en tu app en SwiftUI
Aprende a crear UITests usando Xcode en tu app en SwiftUI

Creamos nuestro primer UITest

Lo primero de todo es ir a nuestro listado de ficheros y ver nuestro Target de Tests que ya teníamos, es importante entender que para crear UITests necesitamos crear otro target diferente, es decir, otra carpeta diferente en nuestro listado de ficheros. Vamos a tener el código de producción (de nuestra app), el código de Tests Unitarios, de Tests de integración y Tests de Snapshots, y a parte vamos a tener los UITests en otra carpeta totalmente separada del resto.

¿Cómo creamos esta carpeta? Muy sencillo, nos vamos al menu de Xcode, File -> New -> Target y aquí buscamos UI. Aparecen varias opciones, nosotros vamos a escoger el UI Testing Bundle. Al seleccionar este nuevo target, nos aparece la opción de añadir un nombre, dejamos el que nos recomienda Xcode y le damos a Finish (sobretodo fíjate que en Project y Target aparezca la información correcta).

Al dar a Finish se crea la carpeta nueva, con 2 tests de ejemplo creados. Entramos al NotasUITests y dentro de este fichero vemos código que nos resulta familiar, los métodos setupWithError, tearDownWithError, un ejemplo etc. Vamos a borrar todos los métodos menos el testExample.

Fíjate que dentro de testExample, tenemos una clase que no habíamos visto hasta ahora. Esta clase nos sirve para manipular nuestra app, y en este caso esta instrucción de launch nos sirve para que se abra. Vamos a ejecutar este test para entender mejor qué ocurre. Pero, ¿dónde está el button que aparecía en los otros tests? es decir, el button que nos permitía lanzar el test? Si no aparece no te preocupes, vamos arriba del todo donde siempre seleccionamos el simulador donde queremos compilar, y en este caso seleccionamos Nota. Aquí vamos a crear un nuevo scheme, vamos a crear el scheme de NotasUITests para lanzar estos tests.

Una vez creado el schema, pulsamos COMMAND+U y el simulador vemos que abre la app, cambia de orientación la pantalla, etc. En este caso ha ejecutado los 2 tests de UI que tenemos creados. Vamos a borrar el NotasUITestsLaunchTests y vamos a entender cómo empezar a crear un UITest

Record de nuestro UITest

Vamos a crear nuestro primer UITest, pero qué test vamos a crear? una parte fundamental de nuestra app es que un user navegue al CreateNoteView y pueda crear una nota. Y al volver que aparezca en el listado y si se selecciona la nota que se navegue para ver el detalle.

Una vez tenemos esto claro, vamos a grabar nuestro primer UITest,vamos a borrar el contenido de testExample y pulsamos en el button rojo que simboliza que vamos a grabar. En nuestro caso Xcode no funciona como debería, así que voy a cerrarlo y volverlo abrir.

Al abrirlo de nuevo fíjate que podemos lanzar el test desde la función (cosa que no aparecía antes), y ahora también podemos pulsar el button de grabar (el button de grabar solo funciona si estamos dentro de una función test)

Pulsamos el button de grabar y vamos a crear una nota desde el simulador. Todo esto mientras estemos grabando estos pasos.

Una vez hemos realizado estos pasos, vamos a pulsar en el stop, y vemos que nuestro test que estaba vacío, ahora tiene una serie de instrucciones:

func testExample() throws {
    let app = XCUIApplication()
    app.launch()
    
    app.toolbars["Toolbar"].staticTexts["Crear Nota"].tap()
    
    let collectionViewsQuery = app.collectionViews
    collectionViewsQuery.textViews["*Título"].tap()
    
    let textoTextView = collectionViewsQuery.textViews["Texto"]
    textoTextView.tap()
    textoTextView.tap()
    app.navigationBars["Nueva Nota"].buttons["Crear Nota"].tap()        
}

Dependiendo de vuestro texto debe varias el resultado, pero fíjate que las instrucciones son claras, vamos a repasarlas

  • Hemos pulsado en el button que tiene de título Crear Nota
  • A continuación, aparece la vista de crear nota y hacemos tap en el textfield del titulo y luego en el de texto.
  • Y finalmente damos al button de crear nota.

Si intentamos pasar nuestro test vemos que funciona, pero no ha añadido texto en nuestra nota. Vamos a añadir este paso en una de las instrucciones de nuestro Test.

Cuando pulsemos en el TextField del título vamos a simular que escribimos el texto de "Suscríbete a SwiftBeta!" para hacerlo es tan sencillo como usar el método typeText, es decir nos quedaría el siguiente resultado:

func testExample() throws {
    let app = XCUIApplication()
    app.launch()
    
    app.toolbars["Toolbar"].staticTexts["Crear Nota"].tap()
    
    let collectionViewsQuery = app.collectionViews
    collectionViewsQuery.textViews["*Título"].tap()
    collectionViewsQuery.textViews["*Título"].typeText("Suscríbete a SwiftBeta!")

Y vamos hacer exactamente lo mismo para el campo del texto, es decir el siguiente campo de nuestro formulario:

func testExample() throws {
    let app = XCUIApplication()
    app.launch()
    
    app.toolbars["Toolbar"].staticTexts["Crear Nota"].tap()
    
    let collectionViewsQuery = app.collectionViews
    collectionViewsQuery.textViews["*Título"].tap()
    collectionViewsQuery.textViews["*Título"].typeText("Suscríbete a SwiftBeta!")
    
    let textoTextView = collectionViewsQuery.textViews["Texto"]
    textoTextView.tap()
    textoTextView.typeText("Aprende a crear aplicaciones en cualquier entorno Apple, usando Swift, SwiftUI, Xcode, etc")
    app.navigationBars["Nueva Nota"].buttons["Crear Nota"].tap()
}

Si ahora ejecutamos nuestro test vemos que cuando aparece la vista de crear una nota nueva, se escriben los valores que hemos especificado en nuestro test. Y el test pasa correctamente.

Optimización de nuestros tests con identificadores

Aquí podríamos mejorar nuestro test para trabajar con identificadores en lugar de valores concretos. Es decir, fíjate que por ejemplo en nuestros textfields, los estamos obteniendo a partir del valor del prompt, pero imagina que cambias este valor, pues rompería el test porque el test no encontraría el antiguo prompt en la pantalla. Lo mejor es usar identificadores para no tener estos problema (y también puede que estés pensando, qué ocurre si tengo la app en varios idiomas? pues con los identificadores también lo solucionarías).

Vamos a crear 2 identificadores para estos dos TextFields. Nos vamos a la vista CreateNoteView y justo debajo de cada uno de ellos usamos el ViewModifier accessibilityIdentifier

TextField("", text: $title, prompt: Text("*Título"), axis: .vertical)
    .accessibilityIdentifier("createnote_title_identifier")
TextField("", text: $text, prompt: Text("Texto"), axis: .vertical)
    .accessibilityIdentifier("createnote_text_identifier")

Ahora sería tan fácil como actualizar nuestro test para que use estos identificadores de la siguiente manera:

func testExample() throws {
    let app = XCUIApplication()
    app.launch()
    
    app.toolbars["Toolbar"].staticTexts["Crear Nota"].tap()
    
    let collectionViewsQuery = app.collectionViews
    let titleTextView = collectionViewsQuery.textViews.matching(identifier: "createnote_title_identifier").element
    titleTextView.tap()
    titleTextView.typeText("Suscríbete a SwiftBeta!")
    
    let textTextView = collectionViewsQuery.textViews.matching(identifier: "createnote_text_identifier").element
    textTextView.tap()
    textTextView.typeText("Aprende a crear aplicaciones en cualquier entorno Apple, usando Swift, SwiftUI, Xcode, etc")
    app.navigationBars["Nueva Nota"].buttons["Crear Nota"].tap()
}   

Mucho más limpio la verdad. Importante destacar que si el identificador no existe tu test no pasará, por eso es importante añadir el identificador correcto.

Acabamos de crear nuestro primer UITest! ahora podríamos jugar y crear otro test que añadiera 2 notas a nuestra base de datos, primero vamos a renombrar nuestro test de testExample a testCreateNote.

Creamos un UITest para añadir 2 notas

Vamos a crear nuestro test para que añada 2 notas, primero creamos el test, lo vamos a llamar testCreatetwoNotes y añadimos el mismo contenido que el test que acabamos de crear:

func testCreateTwoNotes() throws {
    let app = XCUIApplication()
    app.launch()
    
    app.toolbars["Toolbar"].staticTexts["Crear Nota"].tap()
    
    let collectionViewsQuery = app.collectionViews
    let titleTextView = collectionViewsQuery.textViews.matching(identifier: "createnote_title_identifier").element
    titleTextView.tap()
    titleTextView.typeText("Suscríbete a SwiftBeta!")
    
    let textTextView = collectionViewsQuery.textViews.matching(identifier: "createnote_text_identifier").element
    textTextView.tap()
    textTextView.typeText("Aprende a crear aplicaciones en cualquier entorno Apple, usando Swift, SwiftUI, Xcode, etc")
    app.navigationBars["Nueva Nota"].buttons["Crear Nota"].tap()
}

A continuación vamos a añadir las siguiente líneas para simular que creamos más notas. No hace falta grabarlas desde el simulador ya que podemos reaprovechar la lógica creada:

app.toolbars["Toolbar"].staticTexts["Crear Nota"].tap()

_ = collectionViewsQuery.textViews.matching(identifier: "createnote_title_identifier").element
titleTextView.tap()
titleTextView.typeText("Suscríbete a SwiftBeta! 22222")

_ = collectionViewsQuery.textViews.matching(identifier: "createnote_text_identifier").element
textTextView.tap()
textTextView.typeText("Me gusta mucho el canal de SwiftBeta")
app.navigationBars["Nueva Nota"].buttons["Crear Nota"].tap()

Y vamos a pasar nuestro Test. Vemos que pasa! ya hemos creado nuestro 2 UITest.

Ya que estamos aquí, vamos a probar de crear un test que nos borre la última nota insertada.

Creamos otro test para borrar la última nota creada

Creo un nuevo test justo debajo del anterior con el mismo contenido, al test lo voy a llamar testRemoveNote:

func testRemoveNote() throws {
    let app = XCUIApplication()
    app.launch()
    
    app.toolbars["Toolbar"].staticTexts["Crear Nota"].tap()
    
    let collectionViewsQuery = app.collectionViews
    let titleTextView = collectionViewsQuery.textViews.matching(identifier: "createnote_title_identifier").element
    titleTextView.tap()
    titleTextView.typeText("Suscríbete a SwiftBeta!")
    
    let textTextView = collectionViewsQuery.textViews.matching(identifier: "createnote_text_identifier").element
    textTextView.tap()
    textTextView.typeText("Aprende a crear aplicaciones en cualquier entorno Apple, usando Swift, SwiftUI, Xcode, etc")
    app.navigationBars["Nueva Nota"].buttons["Crear Nota"].tap()
    
    app.toolbars["Toolbar"].staticTexts["Crear Nota"].tap()
    
    _ = collectionViewsQuery.textViews.matching(identifier: "createnote_title_identifier").element
    titleTextView.tap()
    titleTextView.typeText("Suscríbete a SwiftBeta! 22222")
    
    _ = collectionViewsQuery.textViews.matching(identifier: "createnote_text_identifier").element
    textTextView.tap()
    textTextView.typeText("Me gusta mucho el canal de SwiftBeta")
    app.navigationBars["Nueva Nota"].buttons["Crear Nota"].tap()
    
    let firstNote = collectionViewsQuery.cells.element(boundBy: 0)
    firstNote.tap()
    app.buttons["Eliminar Nota"].tap()
}

y justo debajo añado que se pulse el primer elemento de mi listado, y al mostrarse la pantalla que se pulse el eliminar nota

Vamos a lanzar nuestro nuevo test para ver si funciona, y funciona perfectamente!

Ahora podrías crear más tests para cubrir la aplicación que hemos creado, incluso puedes añadir Asserts entre las acciones que se graban del simulador para ir comprobando casos más concretos.

Conclusión

Hoy hemos aprendido a cómo crear UITest usando Xcode. Estos tests nos permiten grabar comportamientos de nuestra app que ocurren dentro del simulador para luego poder lanzar exactamente las mismas instrucciones.

También hemos aprendido a como interactuar, añadiendo nuestros propios identificadores para hacer más robustos nuestros tests.

Y hasta aquí el video de hoy!