Contenido del curso
Unit Testing con XCTest
- 2

Pruebas Unitarias con XC Test en Desarrollo de Apps SWIFT
04:07 min - 3

Tests asíncronos con XCTest en Swift
11:22 min - 4

Pruebas Unitarias para View Models en Swift
06:40 min - 5

Testing de Software: Estrategias y Configuración en Xcode
08:37 min - 6

Mocks y stubs para unit tests en Swift
08:58 min - 7

Pruebas Unitarias en Home View Model: Implementación y Validación
10:16 min
Integration testing
UI Testing con SwiftUI y XCTest
Reporte y Optimización de Calidad
Pruebas de integración con SwiftData y ViewModel
Resumen
Las pruebas de integración con SwiftData te permiten validar que la lógica de tu ViewModel funciona correctamente cuando se conecta directamente con la base de datos real, sin depender de mocks ni stubs. Esto es clave para desarrolladores iOS que ya cubrieron pruebas unitarias y quieren un siguiente nivel de confianza en su código.
Antes de escribir el primer test, conviene revisar la capa de servicios. En el proyecto Gastify existe una carpeta services con SDDatabaseService, que contiene las funciones de guardar, actualizar, eliminar y obtener totales. Esa implementación real es la que vamos a probar ahora, en lugar de simularla.
¿Cómo configurar el archivo de pruebas de integración?
Dentro del target de tests creas un nuevo archivo desde Unit Test Template y lo nombras HomeViewModelIntegrationTest. Eliminas el código del template y agregas @testable import Gastify para tener acceso a las clases internas del proyecto.
A partir de ahí defines la estructura básica con setup y teardown, y declaras dos propiedades: el ViewModel y el servicio real de base de datos.
override func setUp()para preparar el entorno antes de cada test.override func tearDown()para limpiar después de cada test.- Una propiedad para el
viewModely otra paradatabaseService. [02:14]
¿Qué diferencia hay entre una prueba unitaria y una de integración? La prueba unitaria aísla la lógica usando mocks, mientras que la de integración conecta el ViewModel con el servicio real para verificar que ambos funcionan juntos.
¿Por qué necesito expectations al inicializar SwiftData?
Como SDDatabaseService está marcado con @MainActor, debe inicializarse dentro de una tarea que corra en el actor principal. Esto obliga a usar expectations en el setUp para esperar la finalización de un proceso asíncrono.
El flujo dentro del setup queda así:
- Crear un
expectationllamadoinitializeDatabaseService. - Lanzar un
Task { @MainActor in ... }donde se instancian el servicio y el ViewModel. - Llamar a
expectation.fulfill()cuando termine la inicialización. - Usar
wait(for:timeout:)con un timeout de 2 segundos como margen prudente. [03:30]
Si pasan los 2 segundos sin que se cumpla el expectation, eso indica un error en la implementación o un problema en tiempo de ejecución.
¿Cómo garantizar la integridad entre tests con clearDatabase?
Uno de los riesgos más grandes en pruebas de integración es que un test contamine al siguiente. Si el test A guarda registros y el test B asume base vacía, el resultado se vuelve impredecible.
La solución es una función auxiliar privada llamada clearDatabase que elimina todos los registros antes de cada test. Se llama justo después de inicializar el databaseService dentro del setUp, usando await self.clearDatabase() porque cada eliminación es asíncrona.
En el tearDown también limpias instancias asignando viewModel = nil y databaseService = nil para liberar memoria entre ejecuciones. [05:12]
¿Por qué limpiar la base de datos en cada setup? Porque cada test debe iniciar con un estado conocido y predecible. Si dejas datos residuales, un test puede pasar o fallar dependiendo del orden en que se ejecute.
¿Qué lógica del ViewModel tiene sentido probar en integración?
La única lógica relevante en pruebas de integración es la que toca la base de datos. Aquí el foco son dos funciones del ViewModel:
getRecordspara validar la obtención de registros.getTotalspara validar la obtención de totales.
¿Cómo escribir el test de getRecords paso a paso?
El patrón es muy similar al de pruebas unitarias, pero ya no necesitas inicializar el ViewModel dentro del test porque eso ocurrió en el setUp.
- Crear un
expectationpara esperar la respuesta asíncrona. - Lanzar un
Taskque llame aviewModel.getRecords(). - Hacer
expectation.fulfill()al terminar. - Esperar con
wait(for:timeout:)hasta 2 segundos. - Validar con un assert que la lista de records no sea nula. [07:48]
Un detalle importante surge al correr el test la primera vez: si agregas un XCTAssertFalse(records.isEmpty) el test fallará. Y tiene sentido, porque clearDatabase se ejecuta antes de cada test, así que la base siempre arranca vacía. Ese assert específico no aplica para este caso y debe eliminarse.
¿Cómo interpreto un test que falla en Xcode? En el reporte de tests, los que salen en rojo te llevan al archivo y línea exacta del fallo, mostrando el mensaje del assert que no se cumplió. Eso te da la pista directa para corregir.
Después de eliminar el assert incorrecto, vuelves al test plan, verificas que HomeViewModelIntegrationTest esté incluido, presionas Command + U y los tests pasan satisfactoriamente.
Habilidades y conceptos clave que se trabajan
En esta práctica se ponen en juego varias destrezas concretas del desarrollo iOS moderno:
- Pruebas de integración: validan la interacción real entre componentes, en este caso ViewModel y
SDDatabaseService[00:08]. - SwiftData: el framework de persistencia de Apple usado para guardar, actualizar, eliminar y consultar registros [00:35].
- @MainActor: atributo que obliga a ejecutar código en el actor principal, lo que cambia cómo inicializas servicios en los tests [03:10].
- XCTestExpectation: mecanismo de XCTest para esperar resultados asíncronos antes de hacer asserts [03:30].
- setUp y tearDown: métodos que preparan y limpian el entorno antes y después de cada test, garantizando aislamiento [02:14].
- @testable import: directiva que expone símbolos internos del módulo al target de pruebas [01:50].
La tarea final es implementar el test de getTotals siguiendo el mismo patrón. Cuéntame en los comentarios cómo te fue con esa implementación y si encontraste alguna diferencia respecto a getRecords.