← Back to blog

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.