Andrew Ebling
Andrew Ebling

Reputation: 10283

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

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.

Upvotes: 12

Views: 4152

Answers (7)

gngrwzrd
gngrwzrd

Reputation: 6022

Using Bruno's suggestion I created these fake objects. It overrides the properties that I needed to and adds a helper to make the objects using class_createInstance.

class FakeUNNotificationResponse: UNNotificationResponse {

    override var notification: UNNotification {
        _notification ?? super.notification
    }

    var _notification: FakeUNNotification?

    static func make(request_content_userInfo: [AnyHashable: Any]) -> FakeUNNotificationResponse {
        let response = class_createInstance(FakeUNNotificationResponse.self, 0) as! FakeUNNotificationResponse
        let notification = class_createInstance(FakeUNNotification.self, 0) as! FakeUNNotification
        let content = class_createInstance(FakeUNNotificationContent.self, 0) as! FakeUNNotificationContent
        let request = class_createInstance(FakeUNNotificationRequest.self, 0) as! FakeUNNotificationRequest
        response._notification = notification
        notification._request = request
        request._content = content
        content._userInfo = request_content_userInfo
        return response
    }

}

class FakeUNNotificationRequest: UNNotificationRequest {
    override var content: UNNotificationContent {
        _content ?? super.content
    }

    var _content: FakeUNNotificationContent?
}

class FakeUNNotificationContent: UNNotificationContent {
    override var userInfo: [AnyHashable: Any] {
        _userInfo ?? super.userInfo
    }

    var _userInfo: [AnyHashable: Any]?
}

class FakeUNNotification: UNNotification {
    override var request: UNNotificationRequest {
        _request ?? super.request
    }

    var _request: FakeUNNotificationRequest?
}

Usage example:

let info = ["url": "https://mydeeplink.com/"]
let response = FakeUNNotificationResponse.make(request_content_userInfo: info)
delegate.userNotificationCenter(UNUserNotificationCenter.current(), didReceive: response) { }

Upvotes: 0

Bruno Garelli
Bruno Garelli

Reputation: 234

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

Upvotes: 1

Ilias Karim
Ilias Karim

Reputation: 5397

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 :)

Upvotes: 1

Andrew Dunn
Andrew Dunn

Reputation: 755

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).

Upvotes: 2

sgl0v
sgl0v

Reputation: 1417

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

Upvotes: 3

acastano
acastano

Reputation: 807

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

Upvotes: 11

egarc
egarc

Reputation: 1864

It appears you can initialize UNNotificationContent objects. I've chosen to rework my push handling methods to take UNNotificationContent objects instead of UNNotificationResponse/UNNotification.

Upvotes: 1

Related Questions