Test de Integración en Swift y SwiftUI con SwiftData
Test de Integración en Swift y SwiftUI con SwiftData

Introducción INTEGRATION TESTS con SWIFTDATA en SWIFT (Parte 4)

Los Tests de Integración en Swift nos permiten testar como se comportan diferentes componentes cuando están conectados. Es decir, nos permiten validar que se comportan tal y como deberían. En nuestra app de notas vamos a añadir SwiftData y vamos a crear varios UseCases.

SwiftBeta

Tabla de contenido


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

Hoy en SwiftBeta vamos a continuar con nuestra aplicación de notas y vamos a aprender a crear tests de integración. Dentro de la pirámide del testing, hemos visto los Test Unitarios, que nos permiten testear partes muy pequeñas de nuestro código de forma totalmente independiente, lo vimos testeando el ViewModel y las funciones que teníamos en él por separado (crear, actualizar y borrar nota), y hoy toca ver la siguiente capa, los Tests de integración.

La pirámide de Testing es una idea usada en el mundo del software para mostrar cómo deben repartirse los tests de tu aplicación. Básicamente, en un proyecto, debes tener muchos tests pequeños para partes específicas del código y luego otros tests que vean cómo esas partes funcionan juntas.:

  • Los Tests Unitarios se enfocan en pequeñas partes del código, como podrían ser funciones o métodos. También son rápidos de ejecutar, y se suelen ejecutar con mucha frecuencia. Están abajo de la pirámide, indicando que son la base, deberían haber más test unitarios que cualquier otro tipo de test.
  • Los Tests de Integración se enfocan en diferentes partes de tu aplicación que trabajan juntas. Son más lentas que las pruebas unitarias, pero aún así deberían ser bastante frecuentes. Están en medio de la pirámide, lo que indica que debería haber menos tests de integración que pruebas unitarias.

En el video de hoy, vamos a seguir con la aplicación que construimos en los anteriores videos y la vamos a mejorar, vamos a añadir persistencia de datos con SwiftData. Es decir, vamos a añadir la funcionalidad de guardar las notas en una base de datos local, así si cierro la aplicación y la vuelvo abrir conservo las notas creadas.

Tests de Integración en Swift y SwiftUI en nuestra aplicación de Nota con SwiftData
Tests de Integración en Swift y SwiftUI en nuestra aplicación de Nota con SwiftData

A parte, vamos a crear más capas en nuestra arquitectura. Hasta ahora solo tenemos un ViewModel con 3 métodos. Hoy, vamos a crear 4 casos de uso que vamos a inyectar a nuestro ViewModel. Es decir, vamos a tener la siguiente estructura:

  • View
  • ViewModel
  • UseCases
  • Base de Datos

Cuando nosotros demos al button de crear una nueva nota, la View llamará al ViewModel, y el ViewModel llamará al UseCase de crear una nota nueva. Y finalmente el UseCase, el caso de uso llamará a la clase que maneja la base de datos para crear la nota. ¿Por qué hacemos todo esto? Este es un ejemplo, podrías hacerlo como quisieras, pero en este video vamos a extraer toda la lógica del ViewModel, y por cada función que teníamos antes, ahora vamos a crear un tipo nuevo, en total vamos a crear 4 casos de Uso:

  • Caso de uso para Obtener todas las notas
  • Caso de uso para Crear una nota
  • Caso de uso para Actualizar una nota
  • Caso de uso para Borrar una nota

Al hacerlo de esta manera desacoplamos toda la lógica que teníamos en el ViewModel, y seguimos uno de los principios SOLID, el de Responsabilidad Única.

Vas a ver lo sencillo que es. Y vas a ver cómo al crear los tests de integración testeamos desde la capa del ViewModel hasta la capa de la base de datos. Con un tests vamos a comprobar que la funcionalidad de la app de notas funciona perfectamente: nuestro test empezará desde el ViewModel, pasará por el UseCase y llegará hasta la base de datos y haremos el camino de vuelta.

Creamos la base de datos con SwiftData

Lo primero de todo que vamos hacer es crear el tipo que se va a encargar de interactuar con la base de datos. Nos vamos al listado de ficheros y pulsamos COMMAND+N. Vamos a llamar a nuestro nuevo fichero NotesDatabase.

Una vez creado lo siguiente que vamos hacer es crear la configuración de nuestra base de datos y añadir los métodos de obtener todas las notas, crear nota, modificar nota y eliminar nota.

Vamos a empezar por el principio, nuestro nuevo tipo será un Singleton, así solo crearemos una instancia durante la ejecución de nuestra aplicación.

class NotesDatabase {
    static let shared: NotesDatabase = NotesDatabase()

    private init() {}
}

Lo siguiente que vamos hacer que ya vimos en el video de SwiftData, te lo dejo por aquí arriba, es crear un método para crear el container de nuestra base de datos. Básicamente sin container no podemos persistir datos dentro de nuestra aplicación, así que lo voy a crear en el siguiente método estático:

@MainActor
static func setupContainer(inMemory: Bool) -> ModelContainer {
    do {
        let container = try ModelContainer(
            for: Note.self, ModelConfiguration(inMemory: inMemory)
        )
        container.mainContext.autosaveEnabled = true
        return container
    } catch {
        print("Error \(error.localizedDescription)")
        fatalError()
    }
}

Este método lo usamos para crear un ModelContainer, y le pasamos como parámetro si queremos crear nuestra base de datos en memoria o en disco. Dentro de nuestra app, en el código de producción pasaremos este parámetro a false, y en nuestros tests lo pasaremos a true. Más adelante lo explicaré en detalle.

Modificamos el tipo Note

Para arreglar el error del compilador, vamos a añadir el framework SwiftData. Y también vamos a ir a nuestro modelo Note para especificar con la macro model que las instancias de este tipo se pueden persistir en nuestra base de datos, y también vamos a cambiar nuestro tipo de STRUCT a CLASS.

import SwiftData

@Model
class Note: Identifiable, Hashable {

Una vez hecho este cambio, vamos aprovechar y especificar que el campo id va a ser único, y lo vamos a renombrar, lo vamos a llamar identifier:

@Model
class Note: Identifiable, Hashable {
    @Attribute(.unique) var identifier: UUID

y vamos a cambiar todas las propiedades para que sean var en lugar de let (aquí podemos usar el atajo de teclado SHIFT+CONTROL+CLICK):

var title: String
var text: String?
var createdAt: Date

Fíjate que donde antes usábamos id, ahora usamos identifier. Busca todos aquellos lugares donde se queje el compilador:

  • Como por ejemplo en la misma clase Note (en el init, modificamos todos los id por identifier)
  • Nos vamos al ViewModel, y aquí hacemos lo mismo, modificamos todos los id por identifier
  • Y ahora en la vista UpdateNoteView
  • Y por último en ContentView

Ahora si pulsamos COMMAND+B no deberíamos tener ningún error, y si lo tenemos lo fixeamos.

Perfecto, después de esta pequeña mejora, vamos a continuar y volvemos a nuestro tipo NotesDatabase, lo siguiente que vamos hacer es crear nuestra propiedad container que va a llamar al método setupContainer (el método que acabamos de crear). En este caso creamos la siguiente lineas de código:

@MainActor
var container: ModelContainer = setupContainer(inMemory: false)

En esta línea estamos indicando que nuestra base de datos se persistirá en disco en lugar de memoria. Así nuestra apliación podrás guardar las notas y después de cada lanzamiento podremos obtener la información sin perderla.

Ya tenemos nuestro stack creado, ya podemos empezar a insertar, recuperar, modificar y borrar datos. Debemos tener el siguiente código:

class NotesDatabase: NotesDatabaseProtocol {
    static let shared: NotesDatabase = NotesDatabase()
    
    @MainActor
    var container: ModelContainer = setupContainer(inMemory: false)
    
    private init() { }
    
    @MainActor
    static func setupContainer(inMemory: Bool) -> ModelContainer {
        do {
            let container = try ModelContainer(
                for: Note.self, ModelConfiguration(inMemory: inMemory)
            )
            container.mainContext.autosaveEnabled = true
            return container
        } catch {
            print("Error \(error.localizedDescription)")
            fatalError()
        }
    }
}

Creamos método de insertar en la base de datos

Lo siguiente que vamos hacer es crear el método para insertar nuevas notas en la base de datos. Creamos la firma de la función:

@MainActor
func insert(note: Note) throws {
    // TODO:
}

Ahora vamos a acceder al mainContext del container para llamar al método insert y le vamos a pasar la nota que queremos insertar en la base de datos. Todo esto lo tengo explicado en el video que te mencionaba antes de SwiftData, te lo vuelvo a dejar por aquí arriba.

@MainActor
func insert(note: Note) throws {
    container.mainContext.insert(note)
    // TODO:
}

Y lo siguiente que vamos hacer es crear un do catch para ver si al guardar en la base de datos tenemos un error. Y en caso de tenerlo lo capturamos para lanzarlo a la capa superior a nuestro UseCase, mira que sencillo es:

@MainActor
func insert(note: Note) throws {
    container.mainContext.insert(note)
    do {
        try container.mainContext.save()
    } catch {
        print("Error \(error.localizedDescription)")
    }
}

En este caso queremos lanzar un error, pero todavía no los hemos definido. Vamos arriba del todo de nuestro fichero y vamos a crear un enum indicando todos los errores que queremos controlar en nuestra base de datos.

enum DatabaseError: Error {
    case errorInsert
    case errorFetch
    case errorUpdate
    case errorRemove
}

Ahora podemos completar el método insert añadiendo el throw correspondiente, en este caso DatabaseError.errorInsert (aquí podrías crear nombres de errores más elaborados o que te aporten más información)

@MainActor
func insert(note: Note) throws {
    container.mainContext.insert(note)
    do {
        try container.mainContext.save()
    } catch {
        print("Error \(error.localizedDescription)")
        throw DatabaseError.errorInsert
    }
}

Ya tenemos nuestro primer método listo, antes de probar que funciona vamos a crear el método que nos va a obtener todas las notas, vamos a crear la firma de nuestra función justo debajo del método insert que acabamos de crear:

@MainActor
func fetchAll() throws -> [Note] {
    // TODO:
}

Una vez tenemos la firma del método, fíjate que en este caso retornamos un Array de Notas. Para poderle devolver el Array de todas las notas, debemos hacer una búsqueda en nuestra base de datos. Para hacerlo vamos a usar el tipo FetchDescriptor:

@MainActor
func fetchAll() throws -> [Note] {
    let fetchDescriptor = FetchDescriptor<Note>(sortBy: [SortDescriptor<Note>(\.createdAt)])
    ...
}

Ahora vamos hacer como antes, vamos a usar un do catch para hacer un fetch en nuestra base de datos para obtener todas las notas, y en caso de error lanzaremos un error a la capa superior, en este caso a nuestro UseCase (el que crearemos en los próximos minutos).

@MainActor
func fetchAll() throws -> [Note] {
    let fetchDescriptor = FetchDescriptor<Note>(sortBy: [SortDescriptor<Note>(\.createdAt)])
    do {
        return try container.mainContext.fetch(fetchDescriptor)
    } catch {
        print("Error \(error)")
        throw DatabaseError.errorFetch
    }
}

Una vez tenemos estos dos métodos, vamos a crear un protocolo llamado NotesDatabaseProtocol con la firma de los 2 métodos que hemos creado:

protocol NotesDatabaseProtocol {
    func insert(note: Note) throws
    func fetchAll() throws -> [Note]
}

Y vamos hacer que lo conforme nuestra base de datos

class NotesDatabase: NotesDatabaseProtocol {

Creamos UseCase: CreateNoteUseCase

Una vez hecho estos pasos, vamos a probar que nuestra base de datos puede insertar y recuperar notas, luego volveremos a NotesDatabase, pero ahora nos vamos a mover a una capa superior y vamos a crear 2 UseCases. Primero vamos a crear el UseCase para crear una nota, pulsamos COMMAND+N y lo vamos a llamar CreateNoteUseCase, y va a tener una propiedad, una referencia a la base de datos que acabamos de crear:

struct CreateNoteUseCase {
    var notesDatabase: NotesDatabaseProtocol
    
    init(notesDatabase: NotesDatabaseProtocol = NotesDatabase.shared) {
        self.notesDatabase = notesDatabase
    }
    ...
}

Y ahora vamos a crear un método que se va a encargar de crear la nota y pasarsela al método insert de nuestra base de datos, nos quedaría el siguiente código:

struct CreateNoteUseCase {
    var notesDatabase: NotesDatabaseProtocol
    
    init(notesDatabase: NotesDatabaseProtocol = NotesDatabase.shared) {
        self.notesDatabase = notesDatabase
    }
    
    func createNoteWith(title: String, text: String) throws {
        let note: Note = .init(identifier: UUID(),
                               title: title,
                               text: text,
                               createdAt: .now)
        
        try notesDatabase.insert(note: note)
    }
}

¡Ya tenemos nuestro primer UseCase! Cuando lo llamemos con un title y un text se creará una nota en nuestra base de datos. Ahora vamos a crear el siguiente UseCase

Creamose UseCase: FetchAllNotesUseCase

Para comprobar que hemos añadido correctamente una nota en la base de datos, ahora vamos a crear un UseCase llamado FetchAllNotesUseCase para obtener todas las notas de la base de datos, pulsamos COMMAND+N y creamos el nuevo fichero. Vamos hacer exactamente lo mismo que el UseCase anterior, necesitamos una propiedad con una referencia a la base de datos (en realidad a la clase NotesDatabase que maneja todas las operaciones con la base de datos), y un método encargado de llamada al fetchAll de NotesDatabase, nos quedaría el siguiente código:

import Foundation

struct FetchAllNotesUseCase {
    var notesDatabase: NotesDatabaseProtocol
    
    init(notesDatabase: NotesDatabaseProtocol = NotesDatabase.shared) {
        self.notesDatabase = notesDatabase
    }
    
    func fetchAll() throws -> [Note] {
        try notesDatabase.fetchAll()
    }
}

Muy sencillo, acabamos de crear nuestro 2 UseCases. Fíjate que en ningún UseCase hemos hecho referencia a SwiftData, ya que toda esta responsabilidad la hemos delegado en NotesDatabase, es decir el único que sabe cómo almacenar la información y qué método usa (en este caso SwiftData), es NotesDatabase

Por último vamos a inyectar estos UseCases en nuestro ViewModel

Inyectamos CreateNoteUseCase y FetchAllNotesUseCase en ViewModel

Vamos a nuestro ViewModel, y vamos a añadir los 2 UseCases dentro del inicializador del ViewModel, los vamos a inyectar para que el ViewModel los pueda usar. Nos quedaría el siguiente código:

@Observable
class ViewModel {
    var notes: [Note]
    
    var createNoteUseCase: CreateNoteUseCase
    var fetchAllNotesUseCase: FetchAllNotesUseCase
    
    init(notes: [Note] = [],
         createNoteUseCase: CreateNoteUseCase = CreateNoteUseCase(),
        fetchAllNotesUseCase: FetchAllNotesUseCase = FetchAllNotesUseCase()) {
        self.notes = notes
        self.createNoteUseCase = createNoteUseCase
        self.fetchAllNotesUseCase = fetchAllNotesUseCase
    }
...
}

Y ahora vamos a usar estas referencias de los UseCases que hemos inyectado en sus correspondientes métodos. Es decir, dentro del método createNoteWith vamos a llamar al UseCase de Crear Nota, lo vamos hacer dentro de un do catch:

func createNoteWith(title: String, text: String) {
    do {
        try createNoteUseCase.createNoteWith(title: title, text: text)
    } catch {
        print("Error \(error.localizedDescription)")
    }
}

Y ahora vamos a crear un método nuevo que se va a encargar de obtener todas las notas que hay almacenadas en nuestra base de datos:

func fetchAllNotes() {
    do {
        notes = try fetchAllNotesUseCase.fetchAll()
    } catch {
        print("Error \(error.localizedDescription)")
    }
}

Muy sencillo, antes de compilar y probar nuestro código. Cuando nosotros creamos una nueva nota, queremos que se actualize el listado de notas de nuestra app y se vea al momento. Por eso vamos a llamar al fetchAllNotes cuando creemos una nota, es decir, nuestro método quedaría de la siguiente manera:

func createNoteWith(title: String, text: String) {
    do {
        try createNoteUseCase.createNoteWith(title: title, text: text)
        fetchAllNotes()
    } catch {
        print("Error \(error.localizedDescription)")
    }
}

Ahora vamos a compilar y probar nuestra aplicación. Si creamos una nota vamos a ver qué ocurre, pues parece que funciona pero si compilamos de nuevo no aparece en el listado de notas. Esto significa que no hemos persistido la información en la base de datos? No, lo único es que no estamos haciendo un fetch cuando inicializamos nuestro ViewModel. Para arreglarlo nos vamos al init y justo al final añadimos la siguiente línea de código:

fetchAllNotes()

Vamos a volver a compilar para ver qué ocurre. Al compilar vemos que la nota que hemos creado en la anterior ejecución de nuestra app aparece sin problemas 🚀

Muy bien, tenemos 2 UseCases, uno que crea la nota, y el otro que obtiene todas las notas de nuestra base de datos. Antes de crear los UseCases que nos quedan pendientes, el de actualizar una nota y borrar una nota. Ahora lo que vamos hacer es añadir los Tests de Integración, vamos a validar que cuando se llama a nuestro ViewModel para crear una nota, la nota efectivamente se persiste en la base de datos. Y que cuando llamamos al ViewModel para obtener todas las notas, efectivamente se recuperan todas las notas almacenas en la base de datos.

Creamos los Tests de Integración

Nos vamos al target de tests, y aquí vamos a crear un nuevo fichero llamado ViewModelInte grationTests, pulsamos COMMAND+N y muy importante que al crear este nuevo fichero selecciona el target de testing, el de NotasTests.

Una vez creado hacemos limpieza de los 2 últimos métodos y los comentarios.

import XCTest

final class ViewModelIntegrationTests: XCTestCase {

    override func setUpWithError() throws {
        
    }

    override func tearDownWithError() throws {
        
    }
}

Y vamos a ver cómo empezamos, en este caso queremos crear un Test de Integración para comprobar que el ViewModel, CreateNoteUseCase y Base de Datos funcionan bien. Es decir, que al llamar al ViewModel para crear una nota, el ViewModel llama al UseCase, y el UseCase a la base de datos, y que durante este trayecto el comportamiento es crear una nota en la base de datos. Pues lo primero de todo que vamos hacer es crear una propiedad que sea el ViewModel, el elemento que queremos testear.

final class ViewModelIntegrationTests: XCTestCase {
    var viewModel: ViewModel!
...
}

Al hacerlo vemos que tenemos el error de que no encuentra ViewModel en el scope. Vamos a importar la app principal usando @testable:

@testable import Notas

Así el error desaparece. En nuestro caso vamos a testear que cuando se llama al ViewModel para insertar en la base de datos, obtenemos un comportamiento específico. Podemos usar una terminología muy usada en testing, que es que cuando se quiere testear un componente en concreto, en este caso el ViewModel, podemos llamarlo sut, que es una sigla de System Under Test. De esta manera podemos tener visible en todo momento qué se está testeando en nuestro test (muy útil también cuando viene otro compañero desarrollador a ver el test o actualizarlo, así coge contexto al momento). Voy a renombrar viewModel por sut.

import XCTest
@testable import Notas

final class ViewModelIntegrationTests: XCTestCase {
    var sut: ViewModel!

Lo siguiente que vamos hacer es muy importante, vamos a crear la configuración de nuestra base de datos cada vez que se ejecute un test. Y vamos hacer que nuestra base de datos esté en memoria, solo queremos tener la base de datos activa cuando estemos lanzando los tests.

Primero creamos una constante de database para almacenar su referencia

override func setUpWithError() throws {
    let database = NotesDatabase.shared

Luego creamos el container, como te mencionaba hace unos minutos, sin container no hay base de datos. Y en este caso especificamos que queremos crear nuestro container en memoria:

override func setUpWithError() throws {
    let database = NotesDatabase.shared
    database.container = NotesDatabase.setupContainer(inMemory: true)

Y el siguiente paso es crear una instancia de nuestro ViewModel, de nuestro sut. Para hacerlo le inyectamos los 2 UseCases al ViewModel:

override func setUpWithError() throws {
    let database = NotesDatabase.shared
    database.container = NotesDatabase.setupContainer(inMemory: true)
    
    let createNoteUseCase = CreateNoteUseCase(notesDatabase: database)
    let fetchAllNotesUseCase = FetchAllNotesUseCase(notesDatabase: database)
    
    sut = ViewModel(createNoteUseCase: createNoteUseCase,
                    fetchAllNotesUseCase: fetchAllNotesUseCase)
}

Una vez hecho estos pasos, ya estamos listos para crear nuestro primer test de integración. Pero antes, vamos a arreglar el error del compilador, en este caso debemos especificar que nuestros tests se van a lanzar en el main thread.

@MainActor
final class ViewModelIntegrationTests: XCTestCase {

Vamos a probar que nuestro ViewModel se integra bien con el UseCase y la Base de Datos. Vamos a llamar a nuestro primer test testCreateNote().

func testCreateNote() {
...
}

Dentro de este test, queremos comprobar que al llamar al método de nuestro ViewModel, el comportamiento esperado es que una nota haya sido insertada en la base de datos, igual que vimos en el video de Test Unitarios, vamos a usar XCTAssertEqual y en este caso también XCTAssetNotNil, también vamos a usar el patrón Given, When, Then.

func testCreateNote() {
    // Given
    sut.createNoteWith(title: "Hello 1", text: "text 1")
    // When
    let note = sut.notes.first
    // Then
    XCTAssertNotNil(note)
    XCTAssertEqual(note?.title, "Hello 1")
    XCTAssertEqual(note?.text, "text 1")
    XCTAssertEqual(sut.notes.count, 1, "Debería haber una nota en la base de datos")
}

Vamos a pasar los tests a ver qué ocurre. Vemos que al compilar el tests tenemos un error en los tests unitarios del ViewModel. Esto es porque hemos renombrado id por identifier, vamos a corregirlo. Al fixear todos los nombres, volvemos a pasar el nuevo test que hemos creado. Y pasa correctamente, de momento vamos bien, vamos a crear otro tests para ver qué ocurre cuando creamos 2 notas en nuestra base de datos:

func testCreateTwoNotes() {
    // Given
    sut.createNoteWith(title: "Hello 1", text: "text 1")
    sut.createNoteWith(title: "Hello 2", text: "text 2")
    
    // When
    let firstNote = sut.notes.first
    let lastNote = sut.notes.last
    
    // Then
    XCTAssertNotNil(firstNote)
    XCTAssertEqual(firstNote?.title, "Hello 1")
    XCTAssertEqual(firstNote?.text, "text 1")
    XCTAssertNotNil(lastNote)
    XCTAssertEqual(lastNote?.title, "Hello 1")
    XCTAssertEqual(lastNote?.text, "text 1")
    XCTAssertEqual(sut.notes.count, 2, "Debería haber una nota en la base de datos")
}

Si lanzamos los tests vemos que funciona perfectamente, los 2 tests pasan. Antes de continuar, fíjate que si no estuviera en memoria la base de datos, los datos se almacenarían en disco y harían fallar lo tests. Vamos a comprobarlo, si cambiamos que en lugar de que sea en memoria se persista la información en disco, vamos a pasar lo tests a ver qué ocurre.

database.container = NotesDatabase.setupContainer(inMemory: false)

Y efectivamente fallan, ya que los tests no son independientes, hemos creado una base de datos que es compartida entre tests ya que los datos se están leyendo de un fichero en disco en lugar de recrearse la base de datos en cada ejecución de nuestros tests, no se está reseteando el estado antes de cada test. Es más si compilamos nuestra app en el mismo simulador podríamos ver que aparecen las notas creadas de nuestro Tests dentro de nuestra aplicación.

Vamos a dejarlo como lo teníamos. Cambiamos a true nuestro container.

Por último, vamos a crear otro test de integración. Vamos a obtener todas las notas de nuestra base de datos cuando le pedimos esta información al ViewModel y se comunica con el UseCase, y el UseCase se comunica con la Base de datos, vamos a comprobar que esta integración entre estos componentes funciona como debería.

Lo primero de todo vamos a crear la firma de nuestro test:

func testFetchAllNotes() {
...
}

Ahora creamos 2 notas y vemos que se están insertando correctamente en el Array Notes:

func testFetchAllNotes() {
    // When
    sut.createNoteWith(title: "Note 1", text: "text 1")
    sut.createNoteWith(title: "Note 2", text: "text 2")
    
    // Then
    let firstNote = sut.notes[0]
    let secondNote = sut.notes[1]
    
    // Assert
    XCTAssertEqual(sut.notes.count, 2, "There should be two notes in the database")
    XCTAssertEqual(firstNote.title, "Note 1", "First note's title should be 'Note 1'")
    XCTAssertEqual(firstNote.text, "text 1", "First note's text should be 'text 1'")
    XCTAssertEqual(secondNote.title, "Note 2", "Second note's title should be 'Note 2'")
    XCTAssertEqual(secondNote.text, "text 2", "Second note's text should be 'text 2'")
}

He de decir que esta API que estamos usando del ViewModel, tu podrías crear la tuya propia siguiendo otra implementación. Por ejemplo, en nuestro caso cada vez que se inserta una nota en la base de datos automáticamente se hace un fetch, y por lo tanto se actualiza el Array notes del ViewModel. Tu en tu código podrías seguir otro razonamiento, hay muchas maneras de crear código. Lo importante es que te quede un código que sea fácil de entender, y para el ejemplo de este video es suficiente.

Vamos a pasar los tests de ViewModelIntegrationTests, para lanzarlo pulsamos en el button que está al lado de su firma (así como hemos visto pasaremos todos la suite de tests). Perfecto han pasado todos.

En cambio si pulsamos COMMAND+U, fíjate que nos fallan los de la anterior video, los Unit Tests, esto es debido a que hemos hecho un cambio de implementación en nuestro ViewModel, y los tests nos están indicando que algo ya no se comporta como debería. Vamos a ver qué ocurre.

  • Es normal que los test de Update y Remove passen o no ya que no hemos creado los UseCases correspondientes y el ViewModel y hay un lío de lógica. Vamos a comentarlos.
  • Nos fijamos en los 2 tests que quedan, en este caso no pasan ya que estamos usando la base de datos donde se persisten los datos en disco, y por lo tanto, el estado al lanzar un test se guarda en disco (y esto no es lo que queremos, queremos que los tests sean independientes entre ellos, como lo que acabamos de ver en los tests de integracion que hemos creado una base de datos en memoria)

En estos tests, el ViewModel se está comunicando con el UseCase, y el UseCase con la base de datos. Pero la base de datos está persistiendo los datos en disco, por lo tanto no queremos que ocurra esto en nuestro target de testing. Para arreglar los Unit Tests que tenemos ahora, lo que vamos hacer es mockear los UseCases, vamos a crear unos UseCases que no se conecten a la base de datos, sino que simulen que hacen algo y que nos retornen un resultado que nosotros especifiquemos. Vamos a controlar y simular que recibimos estados o errores de la capa inferior, de la capa de NotesDatabase en nuestro UseCase. Y para hacerlo debemos entender los MOCKS

¡Y esto lo vemos a ver en el siguiente video! También, puedes practicar por tu cuenta e intentar hacer los ejercicios que te propongo a continuación:

  • Crear los UseCases que faltan, en este caso el UseCase de Actualizar una nota y el UseCase de Eliminar una Nota.
  • Y una vez creados, también puedes crear los Tests de Integración para comprobar que la unión de estos componentes funciona y se comporta correctamente dentro de tu aplicación de Notas (lo que hemos visto de que el ViewModel -> UseCases -> Base de datos, están correctamente conectados y el comportamiento es el correcto)

Conclusión

Hoy hemos aprendido a crear UseCases para distribuir todas la responsabilidad de nuestro ViewModel. Hemos creado 2 casos de uso, uno para crear notas en nuestra base de datos, y otro para obtener todas las notas de la base de datos. Una vez creados los hemos inyectado al ViewModel y hemos probado nuestra app. Y una vez implementada toda esta lógica hemos creado los tests de integración para comunicarnos entre estas 3 capas, para crear los tests hemos usado una base de datos en memoria que se resetea cada vez que se lanza un test. De esta manera los tests son independientes entre ellos, eliminando la posibilidad de tener flaky tests debidos a la base de datos.

Y hasta aquí el video de hoy!