DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Last call! Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • 2-Tier Architecture vs 3-Tier Architecture in DBMS
  • The Serious Stack Behind Salesforce Developers — Part 2
  • How to Select an API Management Platform for Your Business
  • How to Build Scalable Mobile Apps With React Native: A Step-by-Step Guide

Trending

  • Start Coding With Google Cloud Workstations
  • Cookies Revisited: A Networking Solution for Third-Party Cookies
  • Issue and Present Verifiable Credentials With Spring Boot and Android
  • Automatic Code Transformation With OpenRewrite

Improving the Testability of CLLocationManager

Learn more about how you can improve the CLLocationManager testability!

By 
Bruno Basas user avatar
Bruno Basas
·
Updated Apr. 04, 19 · Tutorial
Likes (1)
Comment
Save
Tweet
Share
6.1K Views

Join the DZone community and get the full member experience.

Join For Free

It is common to find difficulties with the response of methods that are not ours when we write class tests that have external dependencies. Let’s see a typical case in mobile development where we can find this problem and find out how we can improve the testability of our CLLocationManager.

Improving the Testability of CLLocationManager

We have developed an app with a map that uses the location services of Apple’sCoreLocation to obtain the user’s location.

As we have said, the problem comes when our class uses the CoreLocation framework.
When calling the requestLocation() method of the CLLocationManager class, it tries to retrieve the user’s location and calls our delegated method in the class that the CLLocationManagerDelegate delegate conforms to.

Our intention is to be able to control the values generated by methods, such as requestLocation (), and all this communication via delegates makes the process even more difficult.

Let’s see how we can do this by performing a mock of the external interface of the CLLocationManager using protocols.

We start with our class of location services, which we will call LocationService.
This is a class that has a dependency with CLLocationManager and, in turn, also forms its protocol (CLLocationManagerDelegate) for communication between them.

Basically, the class has the getCurrentLocation method that, for now, asks the framework for the user’s location and we show it in the delegate.

class LocationService: NSObject {

    let locationManager: CLLocationManager
    init(locationManager: CLLocationManager = CLLocationManager()) {
    self.locationManager = locationManager
    super.init()
    self.locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters
    self.locationManager.delegate = self
    }
    func getCurrentLocation() {
    self.locationManager.requestLocation()
    self.locationManager.requestWhenInUseAuthorization()
    }
}

extension LocationService: CLLocationManagerDelegate {

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations:[CLLocation]) {
    guard let location = locations.first else { return }
    print("The location is: \(location)")
    }
    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
    print("Error: \(error)")
    }
}


Starting from our LocationService class, the first thing we are going to do is change how our method of getCurrentLocation returns as the location. And for this, we are going to declare a callback that we will use within our method to return the location.

private var currentLocationCallback: ((CLLocation) -> Void)?

    func getCurrentLocation(completion: @escaping (CLLocation) -> Void) {

    currentLocationCallback = {  (location) in
        completion(location)
    }
    …
}


And in the delegate method, we are going to call the new callback.

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
    guard let location = locations.first else { return }
    self.currentLocationCallback?(location)
    self.currentLocationCallback = nil
    }


As we have said before, our goal is to control the location values returned by the CLLocationManager, and we will write a test to verify it.

    func test_getCurrentLocation_returnsExpectedLocation() {

    let sut = LocationService()
    let expectedLocation = CLLocation(latitude: 10.0, longitude: 10.0)
    let completionExpectation = expectation(description: "completion expectation")

    sut.getCurrentLocation { (location) in
            completionExpectation.fulfill()
            XCTAssertEqual(location.coordinate.latitude,expectedLocation.coordinate.latitude)
            XCTAssertEqual(location.coordinate.longitude,expectedLocation.coordinate.longitude)
    }
    wait(for: [completionExpectation], timeout: 1)
    }


The test fails because, logically, it is obtaining the actual location of the device. We still don’t have a way to inject the value it needs to return.

To solve this, we will start by creating an interface with the same methods that the CLLocationManager class needs.

protocol LocationManagerInterface {
    var locationManagerDelegate: CLLocationManagerDelegate? { get set }
    var desiredAccuracy: CLLocationAccuracy { get set }
    func requestLocation()
    func requestWhenInUseAuthorization()
}



In our class, we must change the references of CLLocationManager to those from our new interface LocationManagerInterface.

   var locationManager: LocationManagerInterface

    init(locationManager: LocationManagerInterface = CLLocationManager()) {
    self.locationManager = locationManager
    super.init()
    self.locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters
    self.locationManager.locationManagerDelegate = self
    }


The default argument of the constructor will not let us change it; it will warn us that the CLLocationManager class does not conform to the new interface that we have created.

We add an extension to the class CLLocationManager that is conforming our protocol to solve it.

extension CLLocationManager: LocationManagerInterface {}


We have a problem with our LocationManagerInterface interface, and that happens because we are still coupled to the CoreLocation system delegate,CLLocationManagerDelegate.

In order to uncouple ourselves from this dependence, we will create our own delegate.

protocol LocationManagerDelegate: class {
    func locationManager(_ manager: LocationManagerInterface, didUpdateLocations locations: [CLLocation])
    func locationManager(_ manager: LocationManagerInterface, didChangeAuthorization status: CLAuthorizationStatus)
}


It will have two methods very similar to those already provided by the CLLocationManager class.

I added the method of didChangeAuthorization as an example to show that the location can be mocked, and we can do it with the other methods available as well.

Now, we can change the CLLocationManagerDelegate of our interface by the one we just created.
The compiler will warn us that we do not comply with the interface again.

So, we assign delegate getters and setters to return our delegate, but nevertheless, we use the CoreLocation system delegate to get the data.

extension CLLocationManager: LocationManagerInterface {
    var locationManagerDelegate: LocationManagerDelegate? {
    get { return delegate as! LocationManagerDelegate? }
    set { delegate = newValue as! CLLocationManagerDelegate? }
    }
}


Now that almost everything is connected, we must change the CLLocationManager protocol information to our protocol.

We will do this in two steps.

First, we make our LocationService class according to our new delegate.

extension LocationService: LocationManagerDelegate {
    func locationManager(_ manager: LocationManagerInterface, didUpdateLocations locations: [CLLocation]) {
    guard let location = locations.first else { return }
    self.currentLocationCallback?(location)
    self.currentLocationCallback = nil
    ...
    }


And second, the thin one already hadCoreLocation; now, it will simply call the one we created.

extension LocationService: CLLocationManagerDelegate {

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
    self.locationManager(manager, didUpdateLocations: locations)
    }
    ...
}


Going back to our test files, as we now have a class that we control, we can create a mock and overwrite the values we want to inject.

   struct LocationManagerMock: LocationManagerInterface {
    var locationManagerDelegate: LocationManagerDelegate?

    var desiredAccuracy: CLLocationAccuracy = 0

    var locationToReturn:(()->CLLocation)?
    func requestLocation() {
            guard let location = locationToReturn?() else { return }
            locationManagerDelegate?.locationManager(self, didUpdateLocations: [location])
        }
    }


We make the necessary changes to our initial test to finally pass the test.

    func test_getCurrentLocation_returnsExpectedLocation() {
    // 1
    var locationManagerMock = LocationManagerMock()

    // 2
        locationManagerMock.locationToReturn = {
        return CLLocation(latitude: 10.0, longitude: 10.0)
    }
    // 3
    let sut = LocationService(locationManager: locationManagerMock)

    let expectedLocation = CLLocation(latitude: 10.0, longitude: 10.0)
    let completionExpectation = expectation(description: "completion expectation")

    sut.getCurrentLocation { (location) in
            completionExpectation.fulfill()
            XCTAssertEqual(location.coordinate.latitude,expectedLocation.coordinate.latitude)
            XCTAssertEqual(location.coordinate.longitude,expectedLocation.coordinate.longitude)
    }
    wait(for: [completionExpectation], timeout: 1)
    }


1: We create the instance of the new mock.

2: We pass the value that we want to return when therequestLocation() method is executed.

3: We inject our mock into the creation of theLocationService. Recall that in the init of the LocationService, we invert the dependencies making the LocationManager according to our interface and not a specific class, which in this case wasCLLocationManager.

In the same way, if we need to control the values of the authorization of localization permits, we can do it in the same way, adding the signature of the method that we want to overwrite to our interface.

If you found this article about CLLocationManager interesting, you might also like:

  • iOS Objective-C app: successful case study
  • iOS snapshot testing
  • How to simplify the data layer with MoyaRx and Codable
  • Espresso Testing
  • Mobile app development trends of the year
  • Banco Falabella wearable case study
  • Mobile development projects
  • Viper architecture advantages for iOS apps
  • Why Kotlin?
Interface (computing) mobile app

Published at DZone with permission of Bruno Basas. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • 2-Tier Architecture vs 3-Tier Architecture in DBMS
  • The Serious Stack Behind Salesforce Developers — Part 2
  • How to Select an API Management Platform for Your Business
  • How to Build Scalable Mobile Apps With React Native: A Step-by-Step Guide

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!