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:
- Extract the required data in as few lines of code as possible.
- Redispatch to a helper method that takes only easily-created parameters.
// 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.