O Desafio dos Sensores de Hardware em Suites de Testes

No desenvolvimento de aplicativos nativos iOS, serviços que dependem de componentes de hardware — como GPS (CoreLocation), câmera (AVFoundation) e sensores de movimento (CoreMotion) — costumam ser os maiores vilões da testabilidade. Quando um desenvolvedor acopla diretamente a classe CLLocationManager a um Presenter ou ViewModel, torna-se praticamente impossível escrever testes unitários determinísticos, rápidos e herméticos.

Testes que exigem hardware real ou simuladores com rotas GPX injetadas manualmente são lentos, frágeis e inviáveis em pipelines de integração contínua (CI/CD). A solução elegante para esse problema está no desacoplamento por meio do Princípio da Inversão de Dependência (DIP).

Diretriz de Design: Suas regras de negócio e apresentadores nunca devem interagir com tipos concretos do framework CoreLocation. Eles devem interagir apenas com abstrações de serviço que emitem coordenadas ou estados de autorização.

Abstraindo o Gerenciador de Localização com Protocolos

O primeiro passo consiste em definir um protocolo que espelha os métodos necessários do CLLocationManager, permitindo que utilizemos tanto a implementação nativa da Apple em tempo de execução quanto um dublê de testes (Mock/Spy) durante a execução da suite de testes:

import Foundation
import CoreLocation

// Contrato para o gerenciador de hardware
public protocol LocationProviderProtocol: AnyObject {
    var delegate: CLLocationManagerDelegate? { get set }
    var authorizationStatus: CLAuthorizationStatus { get }
    func requestWhenInUseAuthorization()
    func startUpdatingLocation()
    func stopUpdatingLocation()
}

// Extensão de conformidade para o tipo nativo da Apple
extension CLLocationManager: LocationProviderProtocol {
    public var authorizationStatus: CLAuthorizationStatus {
        if #available(iOS 14.0, *) {
            return self.authorizationStatus
        } else {
            return CLLocationManager.authorizationStatus()
        }
    }
}

Construção do Serviço de Domínio

Com a abstração criada, construímos nosso LocationService recebendo o provedor injetado via construtor (Injeção por Inicializador):

public protocol LocationServiceDelegate: AnyObject {
    func didUpdateUserCoordinates(latitude: Double, longitude: Double)
    func didFailWithError(_ error: Error)
}

public final class LocationService: NSObject, CLLocationManagerDelegate {
    private let provider: LocationProviderProtocol
    public weak var delegate: LocationServiceDelegate?

    public init(provider: LocationProviderProtocol = CLLocationManager()) {
        self.provider = provider
        super.init()
        self.provider.delegate = self
    }

    public func requestCurrentLocation() {
        provider.requestWhenInUseAuthorization()
        provider.startUpdatingLocation()
    }

    public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let validCoordinate = locations.last?.coordinate else { return }
        provider.stopUpdatingLocation()
        delegate?.didUpdateUserCoordinates(latitude: validCoordinate.latitude, longitude: validCoordinate.longitude)
    }

    public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        delegate?.didFailWithError(error)
    }
}

Criando o Mock e Validando em Testes Unitários

Em nosso módulo de testes (XCTest), criamos um LocationProviderSpy que registra as chamadas e permite simular coordenadas instantaneamente, sem delay de hardware e com assertividade precisa:

import XCTest
@testable import AppDomain

final class LocationProviderSpy: LocationProviderProtocol {
    weak var delegate: CLLocationManagerDelegate?
    var authorizationStatus: CLAuthorizationStatus = .authorizedWhenInUse
    var didCallRequestAuth = false
    var didCallStartUpdating = false
    var didCallStopUpdating = false

    func requestWhenInUseAuthorization() {
        didCallRequestAuth = true
    }

    func startUpdatingLocation() {
        didCallStartUpdating = true
    }

    func stopUpdatingLocation() {
        didCallStopUpdating = true
    }
}

final class LocationServiceTests: XCTestCase {
    func test_requestLocation_shouldTriggerAuthorizationAndUpdates() {
        let spy = LocationProviderSpy()
        let sut = LocationService(provider: spy)

        sut.requestCurrentLocation()

        XCTAssertTrue(spy.didCallRequestAuth, "Deveria solicitar permissão de uso")
        XCTAssertTrue(spy.didCallStartUpdating, "Deveria iniciar busca de coordenadas")
    }
}

Conclusão

Ao isolar bibliotecas nativas de sistema atrás de interfaces limpas, o código torna-se sustentável e resiliente a futuras alterações da própria Apple (como mudanças em assinaturas de APIs em novas versões do iOS). Mais importante: o pipeline de integração contínua passa a rodar centenas de testes unitários em frações de segundo, assegurando a qualidade contínua de funcionalidades críticas dependentes de geolocalização.