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

Viewed 2629

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.

6 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

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
        }
    }
}

If the UNNotificationResponse value doesn't matter, and you just want to execute that method of your app delegate, you can accomplish this by creating a mock by subclassing NSKeyedArchiver like this:

class MockCoder: NSKeyedArchiver {
    override func decodeObject(forKey key: String) -> Any { "" }
}

You can then call it like this:

let notificationMock = try XCTUnwrap(UNNotificationResponse(coder: MockCoder()))

appDelegate.userNotificationCenter(UNUserNotificationCenter.current(), didReceive: notificationMock) { }

Your app delegate's userNotificationCenter(_:didReceive:withCompletionHandler:) method will now have been called, allowing you to assert to your heart's content (assuming no assertions against the notification itself, at least).

You can use class_createInstance(UNNotification.classForKeyedArchiver() ...)and cast the value as UNNotification

If you want to manipulate its content and date members you can subclass UNNotification and use this same formula «changing class name and cast to your subclass» to create it, then you override those members- which are open- and return whatever you want

Related