Pruebas avanzadas en Swift: Unit, Integration y E2E
Las pruebas no son solo una verificación, sino la base de un código confiable. En un nivel avanzado, el desarrollador de Swift no solo necesita escribir pruebas, sino construir correctamente la pirámide de pruebas: desde pruebas unitarias rápidas hasta complejos escenarios end-to-end (e2e). En este artículo, analizaremos técnicas avanzadas para proyectos iOS/macOS: trabajo con dependencias, pruebas de integración con CoreData y automatización de UI.
1. Pruebas unitarias: gestión de dependencias y mocks
En un nivel avanzado, las pruebas unitarias deben estar aisladas. La técnica clave es la Inyección de Dependencias (DI). En lugar de probar la capa de red real o la base de datos, las reemplazamos con mocks.
Ejemplo: protocolo y DI
protocol NetworkServiceProtocol { func fetchData(completion: @escaping (Result<String, Error>) -> Void)}
class ViewModel { private let networkService: NetworkServiceProtocol init(networkService: NetworkServiceProtocol) { self.networkService = networkService } func loadData() { networkService.fetchData { result in // procesamiento } }}
// Mock para pruebasclass MockNetworkService: NetworkServiceProtocol { var shouldReturnError = false func fetchData(completion: @escaping (Result<String, Error>) -> Void) { if shouldReturnError { completion(.failure(NSError(domain: "test", code: -1))) } else { completion(.success("Mock data")) } }}En la prueba, creas un MockNetworkService y lo pasas al ViewModel. Esto permite verificar escenarios de error y éxito sin una API real.
XCTestExpectation para asincronía
func testViewModelLoadDataSuccess() { let mock = MockNetworkService() let viewModel = ViewModel(networkService: mock) let expectation = self.expectation(description: "Data loaded") viewModel.loadData() // Simulamos la llamada asíncrona DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { expectation.fulfill() } waitForExpectations(timeout: 1.0) // Verificamos el estado del ViewModel}2. Pruebas de integración: CoreData y trabajo con datos reales
Las pruebas de integración verifican la interacción entre componentes. Para CoreData, usamos un contenedor en memoria para no contaminar la base de datos principal.
Configuración de CoreData en memoria
class CoreDataTestStack { lazy var persistentContainer: NSPersistentContainer = { let container = NSPersistentContainer(name: "MyModel") let description = NSPersistentStoreDescription() description.type = NSInMemoryStoreType container.persistentStoreDescriptions = [description] container.loadPersistentStores { _, error in if let error = error { fatalError("Failed to load store: \\(error)") } } return container }() var context: NSManagedObjectContext { return persistentContainer.viewContext }}Ejemplo de prueba de integración
func testSaveAndFetchUser() { let stack = CoreDataTestStack() let repository = UserRepository(context: stack.context) // Creamos un usuario let user = User(context: stack.context) user.name = "Test User" user.age = 30 try? stack.context.save() // Verificamos que los datos se hayan guardado let fetchRequest: NSFetchRequest<User> = User.fetchRequest() let users = try? stack.context.fetch(fetchRequest) XCTAssertEqual(users?.count, 1) XCTAssertEqual(users?.first?.name, "Test User")}Importante: después de cada prueba, limpia el contexto para que las pruebas no se afecten entre sí.