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()
    }
}
This Swift code defines a 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)
    }
Validates scenarios where the user grants or denies location services authorization. Also validates location updates.
    @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.

Copyright © 2024-2025 JaviOS. All rights reserved