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.
