This post explains how to validate hardware-dependent components like the LocationManager, which relies on GPS hardware. Testing such managers, including LocationManager and VideoManager, is crucial for addressing challenges developers face, such as hardware constraints, environmental variability, and simulator limitations. By mastering these techniques, you can ensure robust and reliable application behavior in real-world scenarios.
I will guide you through the process of validating a LocationManager, introduce its test support structures, and provide examples of unit tests. Along the way, we’ll explore key techniques like mocking system services, dependency injection, and efficient testing strategies for simulators and real devices.
This improved version enhances clarity, reduces redundancy, and improves flow while retaining all the critical details. Let me know if you'd like further refinements!
Location Manager
In this case, we have a location manager to handle geographic data efficiently and ensure accurate location tracking.
import Foundation
import CoreLocation
@globalActor
actor GlobalManager {
static var shared = GlobalManager()
}
@GlobalManager
class LocationManager: NSObject, ObservableObject {
private var clLocationManager: CLLocationManager? = nil
@MainActor
@Published var permissionGranted: Bool = false
private var internalPermissionGranted: Bool = false {
didSet {
Task { [internalPermissionGranted] in
await MainActor.run {
self.permissionGranted = internalPermissionGranted
}
}
}
}
@MainActor
@Published var speed: Double = 0.0
private var internalSpeed: Double = 0.0 {
didSet {
Task { [internalSpeed] in
await MainActor.run {
self.speed = internalSpeed
}
}
}
}
init(clLocationManager: CLLocationManager = CLLocationManager()) {
super.init()
self.clLocationManager = clLocationManager
clLocationManager.delegate = self
}
func checkPermission() {
clLocationManager?.requestWhenInUseAuthorization()
}
}
extension LocationManager: @preconcurrency CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
let statuses: [CLAuthorizationStatus] = [.authorizedWhenInUse, .authorizedAlways]
if statuses.contains(status) {
internalPermissionGranted = true
Task {
internalStartUpdatingLocation()
}
} else if status == .notDetermined {
checkPermission()
} else {
internalPermissionGranted = false
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
internalSpeed = location.speed
}
private func internalStartUpdatingLocation() {
guard CLLocationManager.locationServicesEnabled() else { return }
clLocationManager?.startUpdatingLocation()
}
}
LocationManager
class that manages location permissions and tracking, integrating with SwiftUI’s reactive model. It uses CLLocationManager
to handle location updates and authorization, updating @Published
properties like permissionGranted
and speed
for UI binding. The class leverages Swift’s concurrency features, including @MainActor
and @globalActor
, to ensure thread-safe updates to the UI on the main thread. Private properties (internalPermissionGranted
and internalSpeed
) encapsulate internal state, while public @Published
properties notify views of changes. By conforming to CLLocationManagerDelegate
, it handles permission requests, starts location updates, and updates speed in response to location changes, ensuring a clean, reactive, and thread-safe integration with SwiftUI.Location Manager
The key is to mock CLLocationManager
and override its methods to suit the needs of your tests:
class LocationManagerMock: CLLocationManager {
var clAuthorizationStatus: CLAuthorizationStatus = .notDetermined
override func requestWhenInUseAuthorization() {
delegate?.locationManager!(self, didChangeAuthorization: clAuthorizationStatus)
}
override func startUpdatingLocation() {
let sampleLocation = CLLocation(
coordinate: CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194),
altitude: 10.0,
horizontalAccuracy: 5.0,
verticalAccuracy: 5.0,
course: 90.0,
speed: 10.0,
timestamp: Date()
)
delegate?.locationManager!(self, didUpdateLocations: [sampleLocation])
}
}
For our test purposes, we are validating the location-granted request service and starting the location update process. During permission validation, we use an attribute to provide the desired response when requestWhenInUseAuthorization
is executed. Additionally, we include a sample CLLocation
to simulate the location data when startUpdatingLocation
is called.
To ensure robust validation of authorization, we have implemented the following unit tests:
@Test func testAthorizacionRequestDenied() async throws {
let locationManagerMock = LocationManagerMock()
locationManagerMock.clAuthorizationStatus = .denied
let sut = await LocationManager(clLocationManager: locationManagerMock)
await sut.checkPermission()
// Wait for the @Published speed property to update
try await Task.sleep(nanoseconds: 1_000_000)
await #expect(sut.permissionGranted == false)
}
@Test func testAthorizacionRequestAuthorized() async throws {
let locationManagerMock = LocationManagerMock()
locationManagerMock.clAuthorizationStatus = .authorizedWhenInUse
let sut = await LocationManager(clLocationManager: locationManagerMock)
await sut.checkPermission()
// Wait for the @Published speed property to update
try await Task.sleep(nanoseconds: 1_000_000)
await #expect(sut.permissionGranted == true)
}
@Test func testStartUpdatingLocation() async throws {
let locationManagerMock = LocationManagerMock()
locationManagerMock.clAuthorizationStatus = .authorizedWhenInUse
let sut = await LocationManager(clLocationManager: locationManagerMock)
await sut.checkPermission()
// Wait for the @Published speed property to update
try await Task.sleep(nanoseconds: 50_000_000)
await #expect(sut.speed == 10.00)
}
Basically we check location speed.
Conclusions
In this post, I have presented a method for validating hardware-dependent issues, such as GPS information. You can find the source code used for this post in the repository linked below.
References
- Michael Katz & Joshua Greene. iOS Test-Driven Development by Tutorials
Kodeko (old Ray Wenderlich) book