Reputation: 10283
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
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
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
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
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
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
}
}
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
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
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