Mark DiFranco
  • Apps
  • Courses
  • Blog
  • Contact

Testing In Swift - Pseudo-Mocking

7/5/2016

0 Comments

 

Let's take a look at a method of mocking out classes in our project, which will make writing tests much easier. The basic premise we’re going to use was covered by Joe Masilotti on his blog.

Swift (at the time of this writing) does not have any official mocking support. So, in the meantime, we’re going to take advantage of extensions to get the job done. The basic idea is: we write a protocol containing all the methods and properties we want to use from an external API, then we extend that API to implement the protocol. Let’s see what that looks like for CLLocationManager:

protocol MDFLocationManager {
    var desiredAccuracy: CLLocationAccuracy { get set }

    func requestWhenInUseAuthorization()
    func startUpdatingLocation()
    func stopUpdatingLocation()
}

extension CLLocationManager: MDFLocationManager { }


Since the methods and properties match exactly, the extension can be empty. This is the ideal case, since it allows us to mock out our dependencies at the lowest level without having to write any code that won’t be tested. That won’t always be the case, however.

What if a class you want to mock happens to have a property whose type also needs mocking? For instance, if we wanted the desiredAccuracy to be of type MDFLocationAccuracy? We can’t just change the type of the desiredAccuracy property, since CLLocationManager will no longer conform to our protocol (since the property can’t have two types). So we’re left with two not-so-great solutions: Name the property something else, or create a function. Since this property requires { get set }, we’ll go with the renamed property:

typealias MDFLocationAccuracy = CLLocationAccuracy

protocol MDFLocationManager {
    var desiredLocationAccuracy: MDFLocationAccuracy { get set }
}

extension CLLocationManager: MDFLocationManager {
    var desiredLocationAccuracy: MDFLocationAccuracy {
        set { desiredAccuracy = newValue }
        get { return desiredAccuracy }
    }
}


Since we’re adding a new property, the CLLocationManager extension can no longer be empty, and now needs to provide an implementation for desiredLocationAccuracy.

What happens when we need the delegate property? We can add it to our protocol, but its type is CLLocationManagerDelegate, and all its methods have a CLLocationManager as the first parameter. This will make testing difficult. But, with a bit more code, we can get around this issue.

typealias MDFLocationAccuracy = CLLocationAccuracy

protocol MDFLocationManager {
    var delegate: MDFLocationManagerDelegate? { get set } // [1]
    var desiredAccuracy: MDFLocationAccuracy { get set }

    func requestWhenInUseAuthorization()
    func startUpdatingLocation()
    func stopUpdatingLocation()
}

// [2]
protocol MDFLocationManagerDelegate: class {
    func locationManager(locationManager: MDFLocationManager, didUpdateLocations locations: [CLLocation])
}

// [3]
class MDFLocationManagerProxy: NSObject, MDFLocationManager, CLLocationManagerDelegate {
    weak var delegate: MDFLocationManagerDelegate?
    var desiredAccuracy: MDFLocationAccuracy {
        set { locationManager.desiredAccuracy = newValue }
        get { return locationManager.desiredAccuracy }
    }
    private let locationManager = CLLocationManager()

    override init() {
        super.init()
        locationManager.delegate = self // [4]
    }

    // [5]
    func locationManager(locationManager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        delegate?.locationManager(self, didUpdateLocations: locations)
    }

    // Squished to save space.
    func requestWhenInUseAuthorization() { locationManager.requestWhenInUseAuthorization() }
    func startUpdatingLocation() { locationManager.startUpdatingLocation() }
    func stopUpdatingLocation() { locationManager.stopUpdatingLocation() }
}


That looks like a lot of code, so let’s step through it:

1. Here we’ve added a delegate property to our MDFLocationManager protocol.

2. Here is where we define our delegate protocol. The methods have the same signature as the CLLocationManagerDelegate methods. This allows us to keep any interesting logic out of this class, which we’ll see below.

3. This is where we create a “proxy” class. It’s main purpose is to act as the CLLocationManagerDelegate, and do as little logic as possible to implement our custom protocols. All code added to this proxy will not be easily testable.

4. Here we make sure to set the underlying location manager’s delegate to our proxy class.

5. Here we listen to any relevant delegate methods, and call the corresponding method on our custom delegate. Since the methods are so similar, we simply need to switch the location manager parameter out and use our proxy instead.

Once this layer is in place, it’s trivial to mock out the dependency for tests like so:

class MDFLocationManagerMock: MDFLocationManager {
    weak var delegate: MDFLocationManagerDelegate?
    var desiredAccuracy: MDFLocationAccuracy = 0

    // Test Properties
    var didRequestWhenInUseAuthorization = false
    var didStartUpdatingLocation = false
    var didStopUpdatingLocation = false

    func requestWhenInUseAuthorization() {
        didRequestWhenInUseAuthorization = true
    }

    func startUpdatingLocation() {
        didStartUpdatingLocation = true
    }

    func stopUpdatingLocation() {
        didStopUpdatingLocation = true
    }    
}


You’d create an instance of the above class in your test, and easily be able to verify how your other code uses it. Things can get a bit out of hand, depending on what API you’re trying to mock. But it’s important to solidify this base layer, since it allows you to test all of the interesting code that will make up your app in an easy and quick way. If I’ve made any mistakes, or if you have any questions, please leave a comment below. Thanks for reading!

0 Comments

    Archives

    July 2016

    Categories

    All
    Swift
    Testing

    RSS Feed

© 2021 Mark DiFranco. All Rights Reserved.
  • Apps
  • Courses
  • Blog
  • Contact