Qué son los mocks en Swift y cómo inyectar dependencias
Qué son los mocks en Swift y cómo inyectar dependencias

¿Qué es un MOCK en TESTING? (Parte 5)

¿Qué son los mocks en Swift? ¿Cómo puedo inyectar dependencias en Swift? Hoy exploramos estos 2 conceptos de testing

SwiftBeta

Tabla de contenido


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

Hoy en SwiftBeta vamos a continuar con nuestra serie de Introducción al Testing en Swift y vamos a aprender a cómo crear nuestros mocks e inyectarlos dentro de nuestra aplicación de notas. En este curso de Testing, hemos aprendido la importancia de crear Tests, y hemos visto de momento 2 tipos de tests para nuestras aplicaciones: los Tests Unitarios y los Tests de Integración. Pero al crear los Tests de Integración en el anterior video hicimos unos cambios en la implementación de nuestro código que hicieron que los Tests Unitarios fallaran (si te acuerdas, creamos diferentes UseCases para tener un código más limpio y así extraer toda la responsabilidad de nuestro ViewModel). Pero ¿por qué fallan? por que al usar nuestro ViewModel, este se comunica con la base de datos real y persiste los datos en disco en lugar de en memoria, y por lo tanto los tests no son independientes entre ellos y acaban fallando. Dentro de nuestro ViewModel, al usar el init, estamos inyectando una instancia de nuestro UseCase real para crear una nota, y otra instancia de nuestro UseCase para obtener todas las notas de la base de datos. Y es justo aquí donde haremos un cambio.
Si te acuerdas lo que dijimos sobre los Tests Unitarios, estos no deben depender de otros componentes (los que si que deben depender son los Tests de Integración), en nuestros Tests Unitarios, nosotros solo queremos testear el comportamiento de nuestro ViewModel, nos da absolutamente igual qué ocurre en capas inferiores como la de la base de datos. Y esto lo vamos a solucionar en el video de hoy.

Hoy vamos a aprender a crear mocks de nuestros UseCases, esto lo podemos representar como una línea que bloquea las demás capas (repito, aquí NO queremos testear el comportamiento que tiene nuestro ViewModel con los demás componentes, ya que para eso ya tenemos los Tests de Integración), queremos testear SOLO nuestro ViewModel. Y para hacerlo especificamos qué queremos recibir, es como engañar a nuestra app (pero solo en el target de testing), para simular diferentes escenarios que se pueden contemplar dentro de nuestra app.

Pero ¿qué beneficios tiene?

  • Nos permite controlar el comportamiento, es decir podemos devolver valores específicos o lanzar errores solo para probar cómo se comportaría nuestro ViewModel en estos escenarios. Podemos probar rápidamente diferentes caminos que puede seguir nuestro código. Es decir, podemos hacer que nuestro CreateNoteUseCase retorne un error cuando queremos crear una nota nueva, o otro ejemplo, podemos retornar el Array de notas que queramos cuando usemos el FetchAllNotesUseCase (y todo esto sin conectarnos a la base de datos)
  • Nos permite también aislar el código que estamos testeando, al usar mocks nos aseguramos que solo estamos testeando el ViewModel, ya que es el tipo que queremos testear. Y aislamos de otros componentes, en este caso la base de datos, ya que pueden influir y hacer que nuestro test no pase.

Los mocks nos permiten crear Tests Unitarios que son rápidos y fiables, donde nos focalizamos en el comportamiento del código que estamos probando. En este caso el ViewModel. En lugar de bajar más capas, vamos a simular que obtenemos ciertos valores de las capas inferiores. Es como si hicieramos un corte, y nosotros controlaramos lo que queremos obtener para ver como se comporta nuestro código. Es decir, podemos decir que nuestro UseCase retorne 10 notas para ver como se comporta nuestra app, o podemos retornar un error específico para ver cómo se comporta. Pero en estos ejemplo nosotros estamos forzando que ocurran. Sé que lo estoy repitiendo, pero este curso es introducción al Testing en Swift y quiero que este concepto quede claro.

Video de la parte #5 del curso de Testing en Swift
Video de la parte #5 del curso de Testing en Swift

Mockear es muy útil incluso cuando queremos hacer una petición HTTP, esto lo vimos en el video de VIPER del canal. De forma resumida, en lugar de que cada vez que quieras probar tus peticiones HTTP, puedes crear tests con los datos mockeados de las respuestas. Así en lugar de realizar la petición HTTP directamente a backend (a tu servidor), puedes probar como se comportaría tu código en caso de recibir el JSON correcto de esa petición HTTP. O mejor aún, puedes probar los diferentes errores que te puede retornar el backend, los puedes mockear, para ver cómo se comportaría tu aplicación (imagina un error de que un usuario no está autenticado, o que un producto no existe en la base de datos), en lugar de hacer peticiones reales y probarlo todo a mano consumiéndote tiempo preparando estos escenarios, puedes crear estos escenarios mockeando los datos de tu JSON. Si quieres más contexto y mejor explicación de este caso en concreto, mírate el video de VIPER que allí lo expliqué cuando hablamos de Inyección de Dependencias (te lo dejo por aquí arriba).

¿Qué necesitamos para poder mockear nuestro UseCase?

Vamos a ver la implementación que hicimos de nuestro CreateNoteUseCase. Vamos a buscarlo en nuestro listado de ficheros. Y ya que estamos aquí vamos a crear una carpeta llamada UseCases, y metemos los 2 UseCases que creamos en el anterior video.

Para poder mockear nuestro CreateNoteUseCase vamos a trabajar en Abstracciones en lugar de implementaciones concretas de un tipo (más adelante lo vas a entender mejor). Si te acuerdas del video anterior, lo hicimos cuando creamos el protocolo NotesDatabaseProtocol, en este caso estábamos creando una abstracción en lugar de trabajar directamente con el tipo NotesDatabase.

Ahora, en lugar de usar directamente CreateNoteUseCase debemos poder simular que estamos trabajando con este tipo, es decir, los mismos métodos, el mismo contrato (hay varias maneras de decirlo). Para hacerlo lo que vamos hacer es crear un protocolo llamado CreateNoteProtocol y le vamos a añadir el método createNoteWith que usamos en el UseCase, quedaría este código:

protocol CreateNoteProtocol {
    func createNoteWith(title: String, text: String) throws
}

Ahora, vamos hacer que este protocolo lo conforme CreateNoteUseCase.

struct CreateNoteUseCase: CreateNoteProtocol {

Esto que acabamos de hacer parece insignificante, pero tiene muchos beneficios y vas a entenderlo en cuanto creemos nuestro primer mock en el target de test, pero para algunos beneficios son:

  • Desacoplamiento, ahora nuestro ViewModel no usará directamente el tipo CreateNoteUseCase, usará una abstracción al inyectarle tipos de CreateNoteProtocol. Piensa en el protocolo como las reglas de un juego de amigo invisible. En este juego se establece un 'contrato' con tres reglas simples: el regalo debe costar menos de 100€, estar relacionado con superhéroes, y venir en una caja. Estas reglas son como un protocolo en programación: definen lo que se necesita, pero no cómo lograrlo.

    En la tienda de cómics, tienes varias opciones que cumplen estas reglas:
    • Un cómic de Thor por 80€, en una caja
    • Una figura de Iron Man por 50€, en una caja
    • Un conjunto de cómic y figura de Spiderman por 90€, en una caja

Cada una de estas opciones es como un tipo en Swift que cumple con el protocolo. Puedes elegir cualquiera, siempre y cuando sigas las 'reglas', el 'contrato' establecido. Del mismo modo en la programación, diferentes clases o estructuras pueden conformar a un mismo protocolo, siempre y cuando implementen los requisitos definidos por ese protocolo. Así, el protocolo permite flexibilidad en la implementación mientras se asegura de que se cumplan ciertos criterios esenciales.

  • Siguiendo con el anterior punto, otros beneficios de usar mocks es que nos facilitan mucho el testing, ya que podemos crear implementaciones mock que conformen esta interfaz, esta abstracción para simular los comportamientos específicos en nuestros tests unitarios. Cualquier tipo que conforme este protocolo, podemos forzarlo para retornar la información que queramos.
  • Y otros beneficios que no entraré en detalle como Polimorfismo, Extensibilidad, Claridad en dependencias, etc

Lo siguiente que vamos hacer es ir al FetchAllNotesUseCase, y vamos a crear otro protocolo llamado FetchAllNotesProtocol y vamos añadir el método como requirimiento de conformar este protocolo:

protocol FetchAllNotesProtocol {
    func fetchAll() throws -> [Note]
}

Y una vez creado, vamos a hacer que lo conforme nuestro UseCase:

struct FetchAllNotesUseCase: FetchAllNotesProtocol {

Una vez creados los protocolos, nos vamos al ViewModel, y en lugar de usar implementaciones concretas de un tipo, vamos a abstraerlo y vamos a usar los protocolos. Así, como te decía nosotros podremos inyectar el tipo que queramos mientras conforme el contrato, el protocolo.

Dentro de nuestro ViewModel, donde especifiquemos:

  • el tipo CreateNoteUseCase vamos a sustituirlo por CreateNoteProtocol
  • ahora donde veamos el tipo FetchAllNotesUseCase, vamos a sustituirlo por el protocolo FetchAllNotesProtocol

Una vez hecho este cambio, puedes pulsar COMMAND+B y ver que sigue compilando perfectamente. Lo único que hemos hecho, es que las propiedades createNoteUseCase y fetchAllNotesUseCase, en lugar de pertenecer su instancia a un tipo en concreto, ahora nos hemos basado en una interfaz, en un protocolo. Es decir, el tipo que guardemos en estas propiedades, debe tener la misma firma que hemos definido en el protocolo.

Para el caso de CreateNoteUseCase, tenemos la misma firma que hemos especificado en CreateNoteProtocol. Y para el caso de FetcAllNotesUseCase tenemos la misma firma que en el protocolo FetchAllNotesProtocol, estamos cumpliendo el contrato. Y si no obtendriamos un error.

Es más, para intentar aclarartelo más, si yo ahora voy al protocolo CreateNoteProtocol y creo un nuevo método, mi código no compilaría ya que el método que he añadido no lo tengo implementado en CreateNoteUseCase).

Si añadimos el siguiente método:

protocol CreateNoteProtocol {
    func createNoteWith(title: String, text: String) throws
    func mySwiftBetaMethod()
}

Automáticamente el compilador se queja y obtenemos este error:

Type 'CreateNoteUseCase' does not conform to protocol 'CreateNoteProtocol'

Vamos a eliminar el método, y vamos a continuar.

Una vez hecho este paso, ya podemos ir a nuestro target de tests y crear nuestro primer mock. Abrimos ViewModelTests y aquí vamos a crear nuestro primer mock, aunque es aconsejable crearlo en otro fichero, de momento lo crearemos aquí (y más adelante lo moveremos a otro fichero).

Creamos nuestro primer mock en Swift

El primer mock que vamos a crear es el comportamiento que queremos tener de CreateNoteUseCase. Vamos a crear un mock de este UseCase para simular que creamos una nota en la Base de datos, pero no va a ser así, estamos preparando esta lógica para testear el ViewModel.
Vamos a crear un nuevo tipo llamado CreateNoteUseCaseMock que conforme el protocolo CreateNoteProtocol.

struct CreateNoteUseCaseMock: CreateNoteProtocol {
    func createNoteWith(title: String, text: String) throws {
        let note = Note(title: title, text: text, createdAt: .now)
        mockDatabase.append(note)
    }
}

¿Por qué queremos que conforme el protocolo CreateNoteProtocol? porque queremos inyectarlo en nuestro ViewModel, nuestro ViewModel ahora espera un tipo que conforme el protocolo CreateNoteProtocol. Y ahora es posible ya que nuestro ViewModel no depende de implementaciones concretas, ahora depende de abstracciones, de interfaces, de protocolos, de contratos. Por eso no tendremos problema ahora de crear nuestro test y engañar a nuestro ViewModel para que use este UseCase en lugar del que interactua directamente con la base de datos.

Si te fijas, el UseCase de crear una nota guarda la nota nueva en una variable que ahora vamos a crear. La variable va a simular el almacenamiento en la base de datos, esto lo hacemos para poder tener una única fuente de información compartida entre los UseCases. Creamos la variable mockDatabase:

var mockDatabase: [Note] = []

El siguiente método que vamos a mockear es el de obtener todas las notas. Creamos un tipo nuevo llamado FetchAllNotesUseCaseMock que conforme el protocolo FetchAllNotesProtocol, y dentro del método vamos a retornar la variable mockDatabase:

struct FetchAllNotesUseCaseMock: FetchAllNotesProtocol {
    func fetchAll() throws -> [Notas.Note] {
        return mockDatabase
    }
}

Ahora podemos arreglar los test, lo primero de todo nos vamos al setupWithError, y aquí va a ocurrir la magia, vamos a inyectar los UseCases que acabamos de mockear. Si te fijas, si pulsas COMMAND+CLICK en el ViewModel, en su inicializador estamos creando instancias de los UseCases sin mockear, pero en este caso volvemos al test, y aquí en el init vamos a especificar que estas instancias sean los mocks que hemos creado.

override func setUpWithError() throws {
    viewModel = ViewModel(createNoteUseCase: CreateNoteUseCaseMock(),
                          fetchAllNotesUseCase: FetchAllNotesUseCaseMock())
}

Y una vez hecho esto, vamos a pasar los tests. Spoiler vamos a tener un error, pero quería que lo vieras. Tenemos un error ya que no estamos reseteando el valor de nuestra variable mockDatabase. Vamos a vacíar esta variable después de cada tests, por fín vamos a usar el método tearDownWithError:

override func tearDownWithError() throws {
    mockDatabase = []
}

Volvemos a pasar los tests y en este caso todo verde. Los mocks que hemos creado están siguiendo el happy path, es decir, el camino donde el resultado siempre es el esperado, pero podríamos crear mocks donde lanzaremos errores. Esto nos serviría para ver cómo se comporta nuestra aplicación. Lo veremos en el siguiente video, cuando creemos el UseCase de borrar una nota, allí crearemos un test que lanzará un error y lo recogeremos en el ViewModel.

Y por los 2 tests que hay más abajo y hemos comentado no te preocupes, ya que de momento no podemos arreglarlos (lo dejamos para el siguiente video). En el próximo video crearemos los UseCases, el UseCase que nos permitirá actualizar/modificar una nota, y el Use Case que nos permitirá borrar una nota. Y es justo lo que te pedí en el anterior video, que por tu cuenta intentes crear los Use Cases que faltan con sus Integration Tests, y ahora los Unit Tests (donde deberás crear los mocks) y si te quedas atacasdo no te preocupes que en el siguiente video lo vamos a arreglar.

Es decir, para el siguiente video puedes:

  • Crear el UseCase de Actualizar una nota
  • Crear el UseCase de Borrar una nota
  • Crear el Integration Test de Actualizar y Borrar nota
  • Arreglar los Unit Tests de Actualizar y Borrar nota

Conclusión

Hoy en SwiftBeta hemos aprendido a cómo crear mocks para usarlos dentro de nuestros Tests Unitarios.