Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mock UNNotificationResponse & UNNotification (and other iOS platform classes with init() marked as unavailable)

Tags:

tdd

swift

mocking

I need to mock UNNotificationResponse and UNNotification so that I can test my implementation of:

func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Swift.Void)

However I can't usefully subclass these classes because init() is specifically marked as unavailable, resulting in compilation errors like this if I try:

/Path/to/PushClientTests.swift:38:5: Cannot override 'init' which has been marked unavailable

What alternate approaches can be taken here? I look into going down the Protocol Oriented Programming route, however since I do not control the API being called, I can't modify it to take the protocols I'd write.

like image 666
Andrew Ebling Avatar asked Nov 22 '17 15:11

Andrew Ebling


3 Answers

To do it you do the following.

Get a real example of the object while debugging and save in file system using your simulator.

func userNotificationCenter(_ center: UNUserNotificationCenter,
                                willPresent notification: UNNotification,
                                withCompletionHandler completionHandler: (UNNotificationPresentationOptions) -> Void) {

let encodedObject = NSKeyedArchiver.archivedData(withRootObject: notification)

let  path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] + "/notification.mock"

fileManager.createFile(atPath: path, contents: encodedObject, attributes: nil)


Find the object in your Mac and add the file in the same target as the test class.

Now unarchive in your test.


let path = Bundle(for: type(of: self)).path(forResource: "notification", ofType: "mock")

let data = FileManager.default.contents(atPath: path ?? "")

let notification = NSKeyedUnarchiver.unarchiveObject(with: data ?? Data()) as? UNNotification

like image 148
acastano Avatar answered Oct 15 '22 07:10

acastano


I've used the next extension to create UNNotificationResponse and UNNotification instances while implementing unit tests for push notifications on iOS:

extension UNNotificationResponse {

    static func testNotificationResponse(with payloadFilename: String) -> UNNotificationResponse {
        let parameters = parametersFromFile(payloadFilename) // 1
        let request = notificationRequest(with: parameters) // 2 
        return UNNotificationResponse(coder: TestNotificationCoder(with: request))! // 3
    }
}
  1. Loads push notification payload from file
  2. Creates UNNotificationRequest instance with specified parameters in userInfo
  3. Creates UNNotificationResponse instance using NSCoder subclass

Here are the functions I've used above:

extension UNNotificationResponse {

    private static func notificationRequest(with parameters: [AnyHashable: Any]) -> UNNotificationRequest {
        let notificationContent = UNMutableNotificationContent()
        notificationContent.title = "Test Title"
        notificationContent.body = "Test Body"
        notificationContent.userInfo = parameters

        let dateInfo = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute, .second], from: Date())
        let trigger = UNCalendarNotificationTrigger(dateMatching: dateInfo, repeats: false)

        let notificationRequest = UNNotificationRequest(identifier: "testIdentifier", content: notificationContent, trigger: trigger)
        return notificationRequest
    }
}

fileprivate class TestNotificationCoder: NSCoder {

    private enum FieldKey: String {
        case date, request, sourceIdentifier, intentIdentifiers, notification, actionIdentifier, originIdentifier, targetConnectionEndpoint, targetSceneIdentifier
    }
    private let testIdentifier = "testIdentifier"
    private let request: UNNotificationRequest
    override var allowsKeyedCoding: Bool { true }

    init(with request: UNNotificationRequest) {
        self.request = request
    }

    override func decodeObject(forKey key: String) -> Any? {
        let fieldKey = FieldKey(rawValue: key)
        switch fieldKey {
        case .date:
            return Date()
        case .request:
            return request
        case .sourceIdentifier, .actionIdentifier, .originIdentifier:
            return testIdentifier
        case .notification:
            return UNNotification(coder: self)
        default:
            return nil
        }
    }
}
like image 35
sgl0v Avatar answered Oct 15 '22 08:10

sgl0v


Short answer: You can't!

Instead, decompose your implementation of

func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Swift.Void)

and test the methods you call from there, instead.

Happy testing :)

like image 36
Ilias Karim Avatar answered Oct 15 '22 09:10

Ilias Karim