Aprende a crear Snapshot Tests en Swift y SwiftUI
Aprende a crear Snapshot Tests en Swift y SwiftUI

SNAPSHOT TESTS en SWIFTUI (Parte 7)

Los Snapshot Tests nos permiten crear capturas de nuestra UI para guardarlas como referencia. De esta manera, cuando pasamos nuestros tests, la captura creada en el test se compara con la de la referencia guardada. Si coincide, el test pasa y sino es que la vista ha sido modificada

SwiftBeta

Tabla de contenido


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

Hoy en SwiftBeta vamos a continuar con nuestra serie de Testing, vamos a aprender a cómo crear Snapshot Tests de nuestras vistas en SwiftUI (este tipo de tests también son llamados Pruebas de Capturas Instantáneas). Pero, ¿por qué debemos crear estos tests? ¿para qué nos sirven? ¿son útiles? Pues sí. Sirven para comparar visualmente la UI de tu aplicación, de esta manera puedes saber si has cometido algún error muy fácilmente.

Video completo para aprender los Snapshot Tests en Swift y SwiftUI
Video completo para aprender los Snapshot Tests en Swift y SwiftUI

Te explico cómo funciona:

  • Primero de todo, creamos el snapshot de nuestra vista. Un snapshot es una simple captura en formato png: Al ejecutar por primera tu test, se crea una captura de pantalla. Esta captura se guarda como referencia (para poder comparar en futuras iteraciones que hagamos de la vista). En nuestro caso, imagina que creamos una captura del listado de notas. Nosotros no debemos hacer estas capturas de forma manual, sino que las hará el test por nosotros más adelante explicaré a cómo hacerlo). Perfecto, ya tenemos el primer componente, que es crear una captura y tener una imagen de nuestra vista.
  • Lo siguiente que vamos a realizar es ejecutar nuestro tests cada vez que cambiemos la vista de la cual hemos creado el snapshot: Al ejecutar tus tests automatizados, se toma una nueva captura de la vista. Por ejemplo, si lanzamos un test del listado de notas obtenemos una captura nueva. Muy bien, y ¿qué hacemos con esta nueva captura? es donde entra en juego la siguiente parte.
  • Comparación de Capturas: La captura que acabas de obtener del test, se compara con la referencia que guardaste la primera vez que creaste la vista. Si hay alguna diferencia visual entre las dos, el test fallará. Es decir, en nuestro caso, comparamos la primera captura de nuestro listado de notas, con la que ha realizado el test, y si hay un error, por mínimo que sea, el test no pasará y nos lanzará el aviso de que algo ha cambiado en nuestra UI.

Estos tipos de tests son muy útiles para ver que no rompemos o alteramos la UI de nuestra app. Y hoy te voy a enseñar en una video muy práctico a cómo incluir en tus tests automatizados los Snapshots tests.

Instalamos dependencias de Point Free

Lo primero de todo que vamos hacer es instalar una dependencia usando Swift Package Manager. Para ser exactos vamos a utilizar la dependencía de Point Free llamada Swift Snapshot Testing, la podéis encontrar en esta URL https://github.com/pointfreeco/swift-snapshot-testing

GitHub - pointfreeco/swift-snapshot-testing: 📸 Delightful Swift snapshot testing.
📸 Delightful Swift snapshot testing. Contribute to pointfreeco/swift-snapshot-testing development by creating an account on GitHub.

Una vez estáis aquí, podéis leer el README para entender, y obtener más contexto. Lo siguiente que vamos hacer es copiar la URL de este repositorio y nos vamos a Xcode. Aquí nos vamos a la sección de Swift Package Manager, y pulsamos en el button +. Ahora pegamos la URL que hemos copiado en el campo del search. Y le damos a Add Package.

Esperamos unos segundos y nos aparece una nueva vista. Aquí, en la fila de SnapshotTesting seleccionamos el Target donde queremos usar este framework, y seleccionamos NotasTests.

Perfecto, acabamos de instalar la depenencia que nos va a permitir añadir los tests de snapshot de nuestra app.

Creamos nuestro primer Snapshot Test en Swift y SwiftUI

Lo siguiente que vamos hacer es crear un test en nuestro Target NotasTests. Aquí pulsamos COMMAND+N y creamos un fichero Unit Test Case Class. Damos un nombre, en este caso vamos a llamar a nuestro Test CreateNoteViewSnapshotTest y le damos a continuar, antes de crearlo nos fijamos que esté en el Target de NotasTests, y le damos a Create.

Ya que estamos aquí vamos a organizar un poco nuestro Tests, voy a crear 3 carpetas:

  • Unit Tests
  • Integration Tests
  • SnapshotTests

Y dentro de cada carpeta vamos a añadir los tests que vimos en los anteriores videos, de esta manera nuestros Tests quedan organizados, separados por tipo.

Una vez hecho este paso, vamos al CreateNoteViewSnapshotTest que acabamos de crear. Aquí vamos a importar la librería que hemos añadido hace unos minutos.

import SnapshotTesting

Y a continuación eliminamos los métodos que no necesitemos y renombramos el testExample a testCreateNoteView, en realidad vamos a borrar todos los métodos menos el que vamos a renombrar:

final class CreateNoteViewSnapshotTest: XCTestCase {
    func testCreateNoteView() throws {

    }
}

Ahora si queremos usar algún tipo de la app Notas, debemos importarlo como vimos en los anteriores videos cuando creamos tests unitarios y tests de integración:

@testable import Notas

Ya estamos listos para crear nuestro primer tests, vamos a crear la implementación de testCreateNoteView. Queremos testear la UI de CreateNoteView cuando no hemos interactuado con ella, es decir que no hemos escrito ninguna nota en el formulario, pues creamos una instancia de CreateNoteView con un ViewModel recién inicializado. Aquí es otro ejemplo de que CreateNoteView tiene como dependencia el ViewModel, y por lo tanto debemos inyectarle una valor.

final class CreateNoteViewSnapshotTest: XCTestCase {
    func testCreateNoteView() throws {
        let createNoteView = CreateNoteView(viewModel: .init())
    }
}

Una vez tenemos la instancia queremos compararla con la referencia que tengamos en nuestro proyecto. Para hacerlo vamos a usar el método assertSnapshot del framework que hemos instalado hace un momento.

final class CreateNoteViewSnapshotTest: XCTestCase {
    func testCreateNoteView() throws {
        let createNoteView = CreateNoteView(viewModel: .init())
        assertSnapshot(of: createNoteView, as: .image)
    }
}

Al crear el tests de snapshot por primera vez, cuando se ejecute la línea del assertSnapshot fallará, pero ¿por qué? porque no va a encontrar ninguna imagen, ninguna referencia de esta vista. Al ejecutar el test por primera vez es cuando se creará el snapshot para compararlo las próximas veces que lancemos nuestros tests automatizados.

Ya verás, vamos a pasar el. tests y vamos a ver qué ocurre. Efectivamente el test ha fallado porque es la primera vez. Y si te fijas el error ya nos lo está mostrando, clicka para abrir y que se vea todo el contenido del error:

testCreateNoteView(): failed - No reference was found on disk. Automatically recorded snapshot: …

open "file:///Users/home/Desktop/Notas/NotasTests/SnapshoTests/__Snapshots__/CreateNoteViewSnapshotTest/testCreateNoteView.1.png"

Re-run "testCreateNoteView" to assert against the newly-recorded snapshot.

En este error vemos que como no había ninguna referencia de imagen para comprar la instancia de CreateNoteView, la ha creado y la ha guardado en esta ruta. Si clickas vemos como aparece el Snapshot creado, este snapshot se utilizará para comprar la vista CreateNoteView con esta imagen y ver si hay algún cambio. Sé que me repito, pero es un concepto que te puede parecer extraño y quiero recalcarlo.

Si ahora volvemos a nuestro test y lo volvemos a pasar, ya verás como esta vez pasa. Lo volvemos a ejecutar y efectivamente ha pasado.

Hacemos fallar nuestro Snapshot Tests

Ahora imagínate que cambias algún componente de la vista CreateNoteView, o no tienes porque ser tu. Si trabajas en un equipo y alguien hace un cambio, como cambiar el color del Text del footer de la vista CreateNoteView, o cualquier otro cambio visual:

Text("*El título es obligatorio")
    .foregroundStyle(.red)

Si ahora pasamos el Snapshot Test, vemos que no pasa. ¿Por qué? porque la referencia que grabamos al crear el primer tests no es igual a lo que tenemos ahora en la vista CreateNoteView (hemos añadido el foregroundStyle red) y aunque tu creas que es un cambio mínimo, el test no pasa para avisarnos de que no es igual la vista actual con la referencia.

Vamos a abrir el error que nos está mostrando Xcode. En este caso nos indica que las referencias no concuerdan, y vemos un @- y un @+, en el @- podemos encontrar la referencia inicial de nuestra vista, y en el @+ vemos el snapshot que ha creado el test de nuestra vista actualizada, y podemos ver el color rojo en el Text del Footer de nuestro Form.

¿Qué ocurre si queremos actualizar un Snapshot Test?

Ahora imagina que diseño cambia una pantalla, en este caso para seguir con el ejemplo, imagina que el Text del Footer debe estar en rojo. Para indicar que queremos utilizar la versión del rojo para futuras ejecuciones de nuestros Tests Automatizados, lo que hacemos es mover la captura que se generaba en la ruta @+, y moverla a la ruta del @-. Cuando un test falla se genera en una carpeta temporal, es por eso que podemos acceder y ver el resultado.

Pues movemos el snapshot, le damos a reemplazar. Y ahora la fuente de la verdad en la que se fijará nuestro test será esta. Vamos a lanzar nuestro Test y vamos a ver que si que pasa ahora. Perfecto, ha funcionado!

Creamos más variaciones de nuestro Snapshot Test

Aquí podríamos testear más vistas como UpdateNoteView, ContentView, o incluso pequeñas partes de estas vistas. Es decir, en lugar de testear la vista entera puedes testear solo partes de ella. En este caso vamos a testear que nuestra vista CreateNoteView, los campos TextField tienen un texto, pero ¿cómo lo hacemos? Dentro del fichero en el que estamos, vamos a crear un nuevo test, una nueva función:

func testCreateNoteViewWithData() throws {
    
}

Y ahora, vamos a crear una instancia de CreateNoteView, y en este caso a parte de pasarle una instancia de ViewModel, también le vamos a pasar el parámetro title y text:

func testCreateNoteViewWithData() throws {
    let createNoteView = CreateNoteView(viewModel: .init(), title: "Suscríbete a SwiftBeta!", text: "Apoya al canal 🎉")
}

Y ahora vamos a usar el mismo método que hemos usado antes llamado assertSnapshot. De esta manera comprobaremos que la instancia que hemos creado de createNoteView, es igual a la que se compara con la referencia.

Vamos a pasar nuestr tests, pero recuerda que es la primera vez que pasamos el test y por lo tanto fallará. Y así es, perfecto. Vamos a ver qué snapshot ha creado.

Fíjate que en este caso el snapshot nos muestra valores dentro del Formulario, para ser exactos nos está mostrando los valores de Suscríbete a SwiftBeta! y Apoya el canal.

Vamos a volver a pasar el test, y vemos que pasa.

Ahora ya tenemos una suite de tests en nuestra app, si pulsamos COMMAND+U vamos a lanzarlos todos! y pasan. Estamos cubriendo nuestra app con muchos y diferentes tipos de tests.

Ahora podemos seguir creando tests de otras vistas, pero esto te lo dejo para que practiques. Si tienes duda añádelo en los comentarios.

Conclusión

Hoy hemos aprendido a crear Snapshot tests, estos tests nos garantizan que no hay ningún cambio en nuestras vistas. Es decir, cualquier cambio, aunque sea muy pequeño, nuestro tests de snapshot nos avisará para saber en qué lugar está el error, y ver si ha sido intencionado o no. Si ha sido intencionado deberemos recrear otra vez el snapshot test para futuras ejecuciones de nuestros tests automatizados.

Y hasta aquí el video de hoy!