Mocks y stubs para unit tests en Swift

Resumen

Aprender a crear mocks y stubs en Swift te permite escribir unit tests aislados y predecibles, sin depender de tu base de datos real. Si trabajas con SwiftData y arquitectura MVVM, dominar estos servicios simulados es clave para validar tu ViewModel con control total sobre las respuestas.

¿Cuál es la diferencia entre mocks y stubs en testing?

Aunque suelen confundirse, cumplen funciones distintas dentro de tus pruebas unitarias.

Los stubs son la versión más simple: devuelven siempre la misma respuesta falsa, sin lógica detrás. Los mocks van un paso más allá. También entregan respuestas simuladas, pero permiten configurar comportamientos, contar cuántas veces se llamó a una función o controlar errores específicos.

¿Qué es un mock en testing? Es un servicio simulado que devuelve respuestas controladas y permite verificar comportamientos, como número de llamadas o manejo de errores.

¿Qué es un stub en testing? Es una implementación falsa que retorna siempre el mismo valor fijo, sin lógica adicional, útil para inyectar dependencias que no vas a usar activamente.

¿Por qué evitar datos aleatorios en tus tests?

La aleatoriedad es el peor enemigo de un unit test. Si tu servicio mock genera registros de forma aleatoria, nunca sabrás si un test falló por un bug o por un dato impredecible.

Por eso, al revisar el MockDatabaseService original que generaba records aleatorios, lo primero fue eliminar esa lógica. En su lugar, se agregaron variables de control que permiten definir exactamente qué quieres que devuelva el servicio en cada prueba.

¿Cómo se configura un MockDatabaseService controlable?

La idea es exponer variables públicas con valores por defecto que puedas modificar antes de cada test. Por ejemplo:

  • Una lista controlada de registros como fetchRecordsResult.
  • Un booleano para saveNewRecordResult.
  • Otro para updateRecordResult y deleteRecordResult.
  • Un valor numérico para totalsResult.

Cada función del protocolo simplemente retorna el valor de su variable correspondiente. Así, cuando inyectas el mock al ViewModel, puedes ajustar esos valores según lo que quieras probar.

swift class MockDatabaseService: DatabaseServiceProtocol { var fetchRecordsResult: [Record] = [] var saveNewRecordResult: Bool = true var updateRecordResult: Bool = true var deleteRecordResult: Bool = true var totalsResult: Double = 0

func fetchRecords() -> [Record] { fetchRecordsResult } func saveNewRecord() -> Bool { saveNewRecordResult } func updateRecord() -> Bool { updateRecordResult } func deleteRecord() -> Bool { deleteRecordResult } func getTotals() -> Double { totalsResult }

}

¿Cómo crear un StubDatabaseService en Swift?

El stub es mucho más simple. No tiene variables modificables ni lógica configurable.

Para crearlo, agrega un nuevo folder llamado StubDatabaseService, dentro un archivo Swift con el mismo nombre, y haz que la clase implemente el protocolo DatabaseServiceProtocol. Cada función devuelve un valor fijo: una lista vacía, false, false, false y 0.

Este servicio sirve para casos donde necesitas inyectar la dependencia pero no vas a interactuar con ella durante el test. Es útil para pruebas básicas o de inicialización.

¿Cómo configurar setup y teardown en XCTest?

Una vez listos los servicios simulados, toca preparar el archivo de pruebas. Dentro de la carpeta GastifyTests, crea un New File from Template, busca test, elige Unit Test Case Class y nómbralo HomeViewModelTests.

Luego importa el proyecto con @testable import Gastify para acceder a todos sus archivos.

¿Cuándo usar setup y teardown sin throws?

Las plantillas por defecto incluyen setUpWithError y tearDownWithError, pero si no necesitas lanzar errores, puedes sobrescribir las versiones simples:

  • override func setUp() con su super.setUp().
  • override func tearDown() con su super.tearDown().

Declara las propiedades como opcionales para poder limpiarlas:

swift var viewModel: HomeViewModel! var mockDatabaseService: MockDatabaseService! var stubDatabaseService: StubDatabaseService!

En el setUp, después del super, inicializa mockDatabaseService y stubDatabaseService. El viewModel no se inicializa aquí, porque cada test decidirá si recibe el mock o el stub según lo que quiera probar.

En el tearDown, asigna nil a las tres propiedades. Esto garantiza que cada test arranque desde un estado limpio, sin variables contaminadas por ejecuciones previas.

Habilidades y conceptos clave de la clase

  • Mocks vs stubs [00:32]: distinción entre respuestas configurables y respuestas fijas dentro de pruebas unitarias.
  • Inyección de dependencias [01:18]: el HomeViewModel recibe un protocolo DatabaseServiceProtocol para desacoplar la lógica.
  • Eliminación de aleatoriedad [01:48]: reemplazar mockRecords aleatorios por listas controladas.
  • Variables de control en mocks [02:18]: propiedades como fetchRecordsResult, saveNewRecordResult o totalsResult permiten ajustar respuestas por test.
  • StubDatabaseService [03:30]: servicio mínimo sin lógica, útil cuando la dependencia no se usa activamente.
  • @testable import [04:42]: directiva que da acceso a los símbolos internos del proyecto desde el target de pruebas.
  • setUp y tearDown [05:00]: ciclo de vida que prepara y limpia el estado de cada caso de prueba en XCTest.
  • Aislamiento de tests [06:12]: limpiar instancias en tearDown evita efectos colaterales entre pruebas.

¿Qué tipo de servicio simulado usarías para probar la función de guardar un nuevo gasto en tu app? Cuéntame en los comentarios cómo configurarías sus variables de control.