Pruebas en Apps iOS — Tests de Integración con 3ros
Guía práctica sobre cómo realizar tests de integración entre código propio y dependencias externas en iOS usando protocolos y mocks.
En el desarrollo de aplicaciones iOS, es fundamental probar cómo nuestro código se integra con dependencias externas. Esto incluye tanto frameworks de Apple como librerías de terceros.
Librerías de APIs de Hardware:
- CoreLocation (geolocalización y movimiento)
- AVFoundation (acceso audio/visual)
- HealthKit (datos de salud del usuario)
- URLSession (comunicación de red)
Librerías Externas:
- Firebase Analytics
- Realm (persistencia de datos)
- Alamofire (networking HTTP)
Programación Orientada a Protocolos
Los protocolos en Swift funcionan similarmente a las interfaces en otros lenguajes de programación orientada a objetos. Un protocolo define un contrato que debe ser respetado por quienes lo implementen.
Aplicaciones útiles de los Protocolos
Polimorfismo: Objetos de diferentes clases pueden responder a las mismas funciones. Cualquier clase que implemente un protocolo específico puede ejecutar sus métodos de forma apropiada.
Herencia múltiple: Una clase puede implementar varios protocolos simultáneamente, permitiendo mayor flexibilidad.
Tests de Integración con Dependencias Externas
En lugar de mockear directamente la dependencia (URLSession, Alamofire), se crea un Wrapper — un adaptador que abstrae las funciones que nuestro código llamará.
NetworkSession Protocol
protocol NetworkSession {
func fetchData(from url: URL) async throws -> (Data, URLResponse)
}
MockNetworkSession
class MockNetworkSession: NetworkSession {
var data: Data?
var response: URLResponse?
var error: Error?
func fetchData(from url: URL) async throws -> (Data, URLResponse) {
if let error = error {
throw error
}
guard let data = data, let response = response else {
throw URLError(.badServerResponse)
}
return (data, response)
}
}
El mock permite simular tres escenarios:
- Fallos: Retornando errores específicos
- Éxito: Retornando datos correctamente
- Estados HTTP: Simulando diferentes códigos de respuesta
Tests de Éxito y Fallo
func testFetchWeatherSuccess() async throws {
let mockSession = MockNetworkSession()
mockSession.data = """
{
"id": "d290f1ee-6c54-4b01-90e6-d701748f0851",
"city": "San Francisco",
"weatherCondition": "Sunny",
"date": "2024-06-12T12:00:00Z"
}
""".data(using: .utf8)
mockSession.response = HTTPURLResponse(
url: url,
statusCode: 200,
httpVersion: nil,
headerFields: nil
)
let service = WeatherService(session: mockSession)
let weather = try await service.fetchWeather(for: url)
XCTAssertEqual(weather.city, "San Francisco")
XCTAssertEqual(weather.weatherCondition, "Sunny")
}
func testFetchWeatherFail() async throws {
let mockSession = MockNetworkSession()
mockSession.error = URLError(.badURL)
let service = WeatherService(session: mockSession)
do {
let weather = try await service.fetchWeather(for: url)
XCTFail("This should fail")
} catch (let e) {
XCTAssertNotNil(e)
}
}
Implementación del Servicio
class WeatherService {
private let session: NetworkSession
init(session: NetworkSession) {
self.session = session
}
func fetchWeather(for url: URL) async throws -> Weather {
let (data, response) = try await session.fetchData(from: url)
let weather = try JSONDecoder().decode(Weather.self, from: data)
return weather
}
}
Wrappers para Dependencias Concretas
URLSessionWrapper
class URLSessionWrapper: NetworkSession {
private let session: URLSession
init(session: URLSession = .shared) {
self.session = session
}
func fetchData(from url: URL) async throws -> (Data, URLResponse) {
return try await session.data(for: URLRequest(url: url))
}
}
AlamofireSessionWrapper
import Alamofire
class AlamofireSessionWrapper: NetworkSession {
func fetchData(from url: URL) async throws -> (Data, URLResponse) {
return try await withCheckedThrowingContinuation { continuation in
AF.request(url).validate().responseData { response in
switch response.result {
case .success(let data):
if let httpResponse = response.response {
continuation.resume(returning: (data, httpResponse))
} else {
continuation.resume(throwing: URLError(.badServerResponse))
}
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
}
Ventajas de Este Enfoque
La principal ventaja es la facilidad de migración. Si la arquitectura técnica requiere cambiar de URLSession a Alamofire, el cambio es mínimo: solo requiere crear un nuevo wrapper. El resto del código permanece intacto porque depende del protocolo, no de la implementación concreta.
Además, si Apple o Alamofire modifican sus APIs, los cambios se centralizan en un único archivo wrapper, evitando modificaciones dispersas en toda la codebase.