Pages

Saturday, November 14, 2015

Testing Delegate Callbacks Without Mocks

The idea of using protocols in place of mocks was one I first saw at WWDC. More Recently, Eli Perkins explains how to use protocols for an instance of UIApplication.
The basic idea is to declare a protocol that exactly defines what methods I am interested in. When I’m registering for notifications at startup time, I’m not interested in 90% of the features of UIApplication. All I really care about is one method, so I’ll make that explicit with a type:
protocol PushNotificationRegistrar {
    func registerUserNotificationSettings(notificationSettings: UIUserNotificationSettings)
}
Since UIApplication already implements registerUserNotificationSettings(_:), I let the type system know that UIApplications conform to PushNotificationRegistrar with an empty declaration:
extension UIApplication: PushNotificationRegistrar {}
Now for my tests, I’m not trying to mock UIApplication and all of its unrelated baggage. I just need an object (class or struct) that provides the single function registerUserNotificationSettings(_:).
For the full explanation, go read Eli Perkin’s post.
So far so great. The part I haven’t seen discussed is the callback. My custom app delegate has a related delegate call:
optional func application(_ application: UIApplication,
didRegisterUserNotificationSettings notificationSettings: UIUserNotificationSettings)
Because that is defined in UIApplication, I can’t change the method signature to take anything other than a UIApplication. Testing that code path requires a mock UIApplication.
I find myself in this position with any of the system delegate calls. Core Bluetooth passes back CBPeripherals, and there’s no easy way to create a valid instance in a test environment without a mocking library.
I haven’t found a perfect solution, but the strategy I’ve come to adopt is to make your delegate callback methods as “thin” as possible. To show what I mean, I’ll use a CoreBluetooh delegate call from CBCentralManager:
optional func peripheral(_ peripheral: CBPeripheral,
     didDiscoverServices error: NSError?)
That callback passes you a CBPeripheral. The peripheral contains a services property containing an array of CBService. Each CBService contains a id property of CBUUID. So just to identify the service, you’re three properties deep into system provided objects.
optional func peripheral(_ peripheral: CBPeripheral,
     didDiscoverServices error: NSError?) {
     let services = peripheral.services
     for service in services {
        let id = service.UUID.UUIDString
        switch id {
            case .BatteryInfo:
                break
            case .ManufacterInfo:
                break
            // and on and on
        }
     }
}
The method can grow to inclued enough complex switching that you definetly want to test it.
Here’s the stragegy I came up with:
  1. Extract the required data in as few lines of code as possible.
  2. Redispatch to a helper method that takes only easily-created parameters.
Here’s my example:
// Declared in CBPeripheralDelegate
func peripheral(peripheral: CBPeripheral,
     didDiscoverServices error: NSError?) {
     let UUIDs = peripheral.services.map { $0.UUID }
     self.handleDiscoveredServicesWithUUIDs(UUIDs)
}

func handleDiscoveredServicesWithUUIDs(UUIDs: [CBUUID]) {
    // complicated switch statement here
}
CBUUIDs are easy to create in a test environment, so it will be easy to test my new function handleDiscoveredServicesWithUUIDs(UUIDs: [CBUUID]) That leaves one line of untested code: let UUIDs = peripheral.services.map { $0.UUID }. Even if you insisted on testing that line, keeping it so functionally constrained makes your mocking job easier. Personally, I probablby wouldn’t bother.