MOVING THE IMPLEMENTATION [requester requestNotificationSettingsWithCompletionHandler:^(id settings) { UNAuthorizationStatus status = settings.authorizationStatus; if (status != UNAuthorizationStatusAuthorized) { NSError *error = [[NSError alloc] initWithDomain:@"UnauthorizedPushError" code:404 userInfo:@{}]; if (self.registrationCompletionHandler) { self.registrationCompletionHandler(nil, error); } // FIXME: Should the `registrationCompletionHandler` be nilled // here, like in all other cases after it has been called? return; } dispatch_async(dispatch_get_main_queue(), ^{ [application registerForRemoteNotifications]; }); }]; 18 — @basthomas
@objc public protocol NotificationRequester { func requestAuthorization( options: UNAuthorizationOptions, completionHandler: @escaping (Bool, Error?) -> Void ) /// This is expected to be a shim around `getNotificationSettings` that is /// usable from Objective-C. Due to selectors, we can not shadow the name /// with only a different type as a closure parameter. func requestNotificationSettings( completionHandler: @escaping (NotificationSettings) -> Void ) func setNotificationCategories(_ categories: Set) } extension UNUserNotificationCenter: NotificationRequester { public func requestNotificationSettings( completionHandler: @escaping (NotificationSettings) -> Void ) { getNotificationSettings(completionHandler: completionHandler) } } 24 — @basthomas
CREATING THE MOCKS class MockRemoteNotificationController: NSObject, XNGApplicationRemoteNotificationsDelegate { enum RequestedViaDeeplink { case notDetermined case `true` case `false` } var didCallRequestAuthorization = false var didCallHandleNotificationAuthorization = false var didCallRequestAuthorizationViaDeeplink: RequestedViaDeeplink = .notDetermined var didCallHandleNotificationAuthorizationViaDeeplink: RequestedViaDeeplink = .notDetermined // omitting `registerForRemoteNotifications`, as it is not used. 26 — @basthomas
THE TESTS func test_thatTheDeeplink_doesNotTriggerRequest_whenNotLoggedIn() { // without a logged in user CurrentUserTestHelper.stubCurrentUserAsNil() // given I call request notifications SettingsModule.requestNotifications( with: mockRemoteNotificationController, user: CurrentUser.current() ) // then XCTAssertFalse(mockRemoteNotificationController.didCallRequestAuthorization) XCTAssertEqual( mockRemoteNotificationController.didCallRequestAuthorizationViaDeeplink, .notDetermined ) } 31 — @basthomas
func test_thatTheDeeplink_doesNotTriggerRequest_whenNotLoggedIn() { // without a logged in user CurrentUserTestHelper.stubCurrentUserAsNil() // given I call request notifications SettingsModule.requestNotifications( with: mockRemoteNotificationController, user: CurrentUser.current() ) // then XCTAssertFalse(mockRemoteNotificationController.didCallRequestAuthorization) XCTAssertEqual( mockRemoteNotificationController.didCallRequestAuthorizationViaDeeplink, .notDetermined ) } 32 — @basthomas
func test_thatHandlingNotificationAuthorization_viaDeeplink_doesTrack() { guard let delegate = ProxyAppDelegate.sharedInstance().remoteNotificationsDelegate else { return XCTFail("Expected to have a non-nil delegate.") } XCTAssertNotNil(delegate.requestAuthorization) // given we request notifications with deeplink = true let mockTracker = MockTracker() delegate.requestAuthorization?( viaDeeplink: true, notificationRequester: mockNotificationRequester, tracker: mockTracker, application: mockApplication ) // it SHOULD track let status = UNAuthorizationStatus.authorized let trackAction = UNUserNotificationCenter.description(for: status) XCTAssertTrue(mockTracker.didLog(origin: trackOrigin, action: trackAction)) } 33 — @basthomas
func test_thatHandlingNotificationAuthorization_viaDeeplink_doesTrack() { guard let delegate = ProxyAppDelegate.sharedInstance().remoteNotificationsDelegate else { return XCTFail("Expected to have a non-nil delegate.") } XCTAssertNotNil(delegate.requestAuthorization) // given we request notifications with deeplink = true let mockTracker = MockTracker() delegate.requestAuthorization?( viaDeeplink: true, notificationRequester: mockNotificationRequester, tracker: mockTracker, application: mockApplication ) // it SHOULD track let status = UNAuthorizationStatus.authorized let trackAction = UNUserNotificationCenter.description(for: status) XCTAssertTrue(mockTracker.didLog(origin: trackOrigin, action: trackAction)) } 34 — @basthomas
TAKEAWAYS ▸ Testing is hard, but never impossible ▸ Writing testable code helps with more than tests ▸ Protocols and default expressions are ok 41 — @basthomas
TAKEAWAYS ▸ Testing is hard, but never impossible ▸ Writing testable code helps with more than tests ▸ Protocols and default expressions are ok ▸ Utilize assertions and preconditions 41 — @basthomas
TAKEAWAYS ▸ Testing is hard, but never impossible ▸ Writing testable code helps with more than tests ▸ Protocols and default expressions are ok ▸ Utilize assertions and preconditions ▸ Keep side effects in check 41 — @basthomas
TAKEAWAYS ▸ Testing is hard, but never impossible ▸ Writing testable code helps with more than tests ▸ Protocols and default expressions are ok ▸ Utilize assertions and preconditions ▸ Keep side effects in check ▸ Mixing Objective-C & Swift is still difficult 41 — @basthomas