Issue #708
The best way to test is to not have to mock at all. The second best way is to have your own abstraction over the things you would like to test, either it is in form of protocol or some function injection.
But in case you want a quick way to test things, and want to test as real as possible, then for some cases we can be creative to mock the real objects.
One practical example is when we have some logic to handle notification, either showing or deep link user to certain screen. From iOS 10, notifications are to be delivered via UNUserNotificationCenterDelegate
@available(iOS 10.0, *)
public protocol UNUserNotificationCenterDelegate : NSObjectProtocol {
@available(iOS 10.0, *)
optional func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void)
}
and all we get is UNNotificationResponse
which has no real way to construct it.
@available(iOS 10.0, *)
open class UNNotificationResponse : NSObject, NSCopying, NSSecureCoding {
// The notification to which the user responded.
@NSCopying open var notification: UNNotification { get }
That class inherits from NSCopying
which means it is constructed from NSCoder
, but how do we init it?
let response = UNNotificationResponse(coder: ???)
NSObject and NSCoder
The trick is, since UNNotificationResponse
is NSObject
subclass, it is key value compliant, and since it is also NSCopying
compliant, we can make a mock coder to construct it
private final class KeyedArchiver: NSKeyedArchiver {
override func decodeObject(forKey _: String) -> Any { "" }
override func decodeInt64(forKey key: String) -> Int64 { 0 }
}
On iOS 12, we need to add decodeInt64
method, otherwise UNNotificationResponse
init fails. This is not needed on iOS 14
UNNotificationResponse
has a read only UNNotification
, which has a readonly UNNotificationRequest
, which can be constructed from a UNNotificationContent
Luckily UNNotificationContent
has a counterpart UNMutableNotificationContent
Now we can make a simple extension on UNNotificationResponse
to quickly create that object in tests
private extension UNNotificationResponse {
static func with(
userInfo: [AnyHashable: Any],
actionIdentifier: String = UNNotificationDefaultActionIdentifier
) throws -> UNNotificationResponse {
let content = UNMutableNotificationContent()
content.userInfo = userInfo
let request = UNNotificationRequest(
identifier: "",
content: content,
trigger: nil
)
let notification = try XCTUnwrap(UNNotification(coder: KeyedArchiver()))
notification.setValue(request, forKey: "request")
let response = try XCTUnwrap(UNNotificationResponse(coder: KeyedArchiver()))
response.setValue(notification, forKey: "notification")
response.setValue(actionIdentifier, forKey: "actionIdentifier")
return response
}
}
We can then test like normal
func testResponse() throws {
let data: [AnyHashable: Any] = [
"data": [
"type": "OPEN_ARTICLE",
"articleId": "1",
"articleType": "Fiction",
"articleTag": "1"
]
]
let response = try UNNotificationResponse.with(userInfo: data)
let centerDelegate = ArticleCenterDelegate()
centerDelegate.userNotificationCenter(
UNUserNotificationCenter.current(),
didReceive: response,
withCompletionHandler: {}
)
XCTAssertEqual(response.notification.request.content.userInfo["type"], "OPEN_ARTICLE")
XCTAssertEqual(centerDelegate.didOpenArticle, true)
}
decodeObject for key
Another way is to build a proper KeyedArchiver
that checks key and return correct property. Note that we can reuse the same NSKeyedArchiver
to nested properties.
private final class KeyedArchiver: NSKeyedArchiver {
let request: UNNotificationRequest
let actionIdentifier: String
let notification: UNNotification
override func decodeObject(forKey key: String) -> Any? {
switch key {
case "request":
return request
case "actionIdentifier":
return actionIdentifier
case "notification":
return UNNotification(coder: self)
default:
return nil
}
}
}
Updated at 2020-12-07 11:49:55