Pruebas en Swift: de la teoría a la práctica con mocks y fixtures
Imagina que estás horneando un pastel y te quedas sin harina. No puedes probar si la crema queda bien hasta que vayas a la tienda. Más o menos así se siente un desarrollador que escribe pruebas unitarias para código que trabaja con la red, base de datos o dependencias complejas. En lugar de esperar al servidor real o a la base de datos real, usamos objetos mock y fixtures en pruebas Swift — son nuestros "sustitutos de la harina" que nos permiten probar piezas de código aisladas aquí y ahora.
En este artículo no solo enumeraremos definiciones. Analizaremos cómo aplicar correctamente las pruebas Swift en la práctica, cuándo usar mocks y cuándo stubs, y cómo los fixtures ayudan a no volverse loco al preparar datos. ¡Allá vamos!
¿Qué son mock, stub y fixture? Aclarando términos
Antes de escribir código, pongámonos de acuerdo en los conceptos. En el mundo de las pruebas unitarias iOS a menudo se confunden mocks y stubs. Recuerda esta regla simple:
- Stub — un objeto que devuelve datos predefinidos. No verifica cómo fue llamado. Simplemente da una respuesta.
- Mock — un objeto que no solo devuelve datos, sino que también verifica si ciertos métodos fueron llamados y con qué parámetros. Es un "espía".
- Fixture — un conjunto de datos preparado de antemano (generalmente JSON o una estructura) que se usa en las pruebas para crear un entorno predecible.
En las pruebas Swift a menudo combinamos estos enfoques. Por ejemplo, cargamos un fixture JSON, lo parseamos y luego reemplazamos la capa de red con un mock que verifica que la solicitud se envió a la URL correcta.
Escribiendo nuestro primer mock en Swift usando protocolos
La forma más común de crear objetos mock en Swift es usar protocolos. En un proyecto real, tienes un servicio que accede a la red. En las pruebas, lo reemplazas por un mock.
Aquí tienes un ejemplo de abstracción:
// Protocolo para la capa de redprotocol NetworkServiceProtocol { func fetchUsers(completion: @escaping (Result<[User], Error>) -> Void)}
// Implementación real (accede a la red)class RealNetworkService: NetworkServiceProtocol { func fetchUsers(completion: @escaping (Result<[User], Error>) -> Void) { // URLSession, Alamofire, etc. }}
// Mock para pruebasclass MockNetworkService: NetworkServiceProtocol { var fetchUsersCalled = false var resultToReturn: Result<[User], Error> = .success([])
func fetchUsers(completion: @escaping (Result<[User], Error>) -> Void) { fetchUsersCalled = true completion(resultToReturn) }}Ahora en la prueba podemos verificar que el método fue llamado:
func testViewModelFetchesUsers() { let mockService = MockNetworkService() mockService.resultToReturn = .success([User(id: 1, name: "Test")])
let viewModel = UserViewModel(networkService: mockService) viewModel.loadUsers()
XCTAssertTrue(mockService.fetchUsersCalled, "El método fetchUsers debería haber sido llamado")}Observa: estamos usando inyección de dependencias Swift — pasamos la dependencia a través del inicializador. Este es un patrón clave para las pruebas.
Fixtures: cómo no escribir 100500 líneas en las pruebas
Cuando pruebas el parseo de JSON o el trabajo con CoreData, necesitas datos reales. Pero generarlos cada vez en el código es un infierno. Aquí es donde los fixtures en pruebas Swift vienen al rescate.
Crea un archivo users_fixture.json en la carpeta de pruebas:
[ {"id": 1, "name": "Alice", "email": "alice@example.com"}, {"id": 2, "name": "Bob", "email": "bob@example.com"}]Aho