Slide 1

Slide 1 text

TESTING THE UNTESTABLE 1 — @basthomas

Slide 2

Slide 2 text

THIS IS A PARADOX 2 — @basthomas

Slide 3

Slide 3 text

TESTING THE SEEMINGLY UNTESTABLE 3 — @basthomas

Slide 4

Slide 4 text

COMMUNICATION IS HARD 4 — @basthomas

Slide 5

Slide 5 text

COMMUNICATION IS HARD 5 — @basthomas

Slide 6

Slide 6 text

TESTING IS HARD 6 — @basthomas

Slide 7

Slide 7 text

WRITING TESTABLE CODE IS HARD 7 — @basthomas

Slide 8

Slide 8 text

TESTABLE CODE IS NICE 8 — @basthomas

Slide 9

Slide 9 text

LET'S JUMP INTO IT. 9 — @basthomas

Slide 10

Slide 10 text

THE JOURNEY 10 — @basthomas

Slide 11

Slide 11 text

THE JOURNEY ▸ What do I want to test? 10 — @basthomas

Slide 12

Slide 12 text

THE JOURNEY ▸ What do I want to test? ▸ How do I want to test it? 10 — @basthomas

Slide 13

Slide 13 text

THE JOURNEY ▸ What do I want to test? ▸ How do I want to test it? ▸ Does this increase my confidence? 10 — @basthomas

Slide 14

Slide 14 text

THE JOURNEY ▸ What do I want to test? ▸ How do I want to test it? ▸ Does this increase my confidence? ▸ ... where do I start? 10 — @basthomas

Slide 15

Slide 15 text

WHAT PROBLEM ARE YOU TRYING TO SOLVE? 11 — @basthomas

Slide 16

Slide 16 text

WHAT DOES THE CURRENT SITUATION LOOK LIKE? 12 — @basthomas

Slide 17

Slide 17 text

! 5 October 2016 13 — @basthomas

Slide 18

Slide 18 text

! NO TESTS IN SIGHT 14 — @basthomas

Slide 19

Slide 19 text

! BREAKING THINGS UP registerForRemoteNotificationsWithCompletion; + requestAuthorization; + handleNotificationAuthorization; 15 — @basthomas

Slide 20

Slide 20 text

MOVING THE IMPLEMENTATION // registerForRemoteNotificationsWithCompletion BOOL shouldImmediatelyRequestNotificationAccess = [ABTestCentral isFeatureEnabled:EnableImmediateNotificationRequest]; if (shouldImmediatelyRequestNotificationAccess || [NSUserDefaults isBrazeDisabled]) { [self requestAuthorization]; } else { [self handleNotificationAuthorization]; } 16 — @basthomas

Slide 21

Slide 21 text

MOVING THE IMPLEMENTATION // requestAuthorization UNAuthorizationOptions options = [UNUserNotificationCenter defaultOptions]; NSMutableSet *categories = [[NSMutableSet alloc] init]; // some userNotification categories [requester setNotificationCategories:categories]; [requester requestAuthorizationWithOptions:options completionHandler:^(BOOL granted, NSError *_Nullable error) { if (self.registrationCompletionHandler && error != nil) { self.registrationCompletionHandler(nil, error); self.registrationCompletionHandler = nil; return; } [self handleNotificationAuthorization]; }]; 17 — @basthomas

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

AND NOW WHAT? 19 — @basthomas

Slide 24

Slide 24 text

I'VE BEEN HIDING SOMETHING. DEPENDENCY INJECTION. 20 — @basthomas

Slide 25

Slide 25 text

- registerForRemoteNotificationsWithCompletion; - requestAuthorization; - handleNotificationAuthorization; + registerForRemoteNotificationsWithCompletion:user:notificationRequester:tracker:application:delegate; + requestAuthorizationViaDeeplink:notificationRequester:tracker:application; + handleNotificationAuthorizationViaDeeplink:notificationRequester:tracker:application; 21 — @basthomas

Slide 26

Slide 26 text

PROTOCOLS, PROTOCOLS, PROTOCOLS 22 — @basthomas

Slide 27

Slide 27 text

@objc public protocol NotificationSettings { var authorizationStatus: UNAuthorizationStatus { get } } extension UNNotificationSettings: NotificationSettings {} 23 — @basthomas

Slide 28

Slide 28 text

@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

Slide 29

Slide 29 text

@objc public protocol ApplicationProtocol { @objc(openURL:options:completionHandler:) func open( _ url: URL, options: [UIApplication.OpenExternalURLOptionsKey: Any], completionHandler completion: ((Bool) -> Void)? ) func registerForRemoteNotifications() } extension UIApplication: ApplicationProtocol {} 25 — @basthomas

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

func requestAuthorization( viaDeeplink requestedViaDeeplink: Bool, notificationRequester requester: NotificationRequester = MockNotificationRequester(), tracker: XNGTrackerProtocol = MockTracker(), application: ApplicationProtocol = MockApplication() ) { didCallRequestAuthorization = true didCallRequestAuthorizationViaDeeplink = requestedViaDeeplink ? .true : .false } func handleNotificationAuthorization( viaDeeplink handlingViaDeeplink: Bool, notificationRequester requester: NotificationRequester, tracker: XNGTrackerProtocol, application: ApplicationProtocol ) { didCallHandleNotificationAuthorization = true didCallHandleNotificationAuthorizationViaDeeplink = handlingViaDeeplink ? .true : .false } 27 — @basthomas

Slide 32

Slide 32 text

class MockNotificationSettings: NotificationSettings { let authorizationStatus: UNAuthorizationStatus init(authorizationStatus: UNAuthorizationStatus) { self.authorizationStatus = authorizationStatus } } 28 — @basthomas

Slide 33

Slide 33 text

class MockNotificationRequester: NotificationRequester { func requestAuthorization( options: UNAuthorizationOptions, completionHandler: @escaping (Bool, Error?) -> Void ) { completionHandler(true, nil) } func requestNotificationSettings( completionHandler: @escaping (NotificationSettings) -> Void ) { completionHandler(MockNotificationSettings(authorizationStatus: .authorized)) } func setNotificationCategories(_ categories: Set) { // } } 29 — @basthomas

Slide 34

Slide 34 text

class MockApplication: ApplicationProtocol { func registerForRemoteNotifications() { // } func open( _ url: URL, options: [UIApplication.OpenExternalURLOptionsKey: Any], completionHandler completion: ((Bool) -> Void)? ) { // } } 30 — @basthomas

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

AND, WELL, THEN WE FOUND A BUG... SO IT WAS FIXED IN A TEST- DRIVEN WAY 35 — @basthomas

Slide 40

Slide 40 text

func test_thatACustomPushPromptInAppMessage_shouldNotShow_ifTheFeatureSwichIsEnabled() { // There is a feature switch, "enable_immediate_notification_request", // that reverts to the old behavior; meaning we should not prompt // the message. let delegate = LocalInAppMessageControllerDelegate() inAppMessageModal.extras = ["custom_push_prompt": "true"] withFeatures([EnableImmediateNotificationRequest]) { XCTAssertEqual(delegate.shouldShowCustomPushPrompt( for: inAppMessageModal, notificationCenter: MockNotificationRequester(authorizationStatus: .authorized)), .shouldNotShow ) XCTAssertEqual(delegate.shouldShowCustomPushPrompt( for: inAppMessageModal, notificationCenter: MockNotificationRequester(authorizationStatus: .denied)), .shouldNotShow ) if #available(iOS 12.0, *) { XCTAssertEqual(delegate.shouldShowCustomPushPrompt( for: inAppMessageModal, notificationCenter: MockNotificationRequester(authorizationStatus: .provisional)), .shouldNotShow ) } XCTAssertEqual(delegate.shouldShowCustomPushPrompt( for: inAppMessageModal, notificationCenter: MockNotificationRequester(authorizationStatus: .notDetermined)), .shouldNotShow ) } } 36 — @basthomas

Slide 41

Slide 41 text

func test_thatACustomPushPromptInAppMessage_shouldNotShow_ifTheFeatureSwichIsEnabled() { // There is a feature switch, "enable_immediate_notification_request", // that reverts to the old behavior; meaning we should not prompt // the message. let delegate = LocalInAppMessageControllerDelegate() inAppMessageModal.extras = ["custom_push_prompt": "true"] withFeatures([EnableImmediateNotificationRequest]) { XCTAssertEqual(delegate.shouldShowCustomPushPrompt( for: inAppMessageModal, notificationCenter: MockNotificationRequester(authorizationStatus: .authorized)), .shouldNotShow ) XCTAssertEqual(delegate.shouldShowCustomPushPrompt( for: inAppMessageModal, notificationCenter: MockNotificationRequester(authorizationStatus: .denied)), .shouldNotShow ) if #available(iOS 12.0, *) { XCTAssertEqual(delegate.shouldShowCustomPushPrompt( for: inAppMessageModal, notificationCenter: MockNotificationRequester(authorizationStatus: .provisional)), .shouldNotShow ) } XCTAssertEqual(delegate.shouldShowCustomPushPrompt( for: inAppMessageModal, notificationCenter: MockNotificationRequester(authorizationStatus: .notDetermined)), .shouldNotShow ) } } 37 — @basthomas

Slide 42

Slide 42 text

❌ 38 — @basthomas

Slide 43

Slide 43 text

func shouldShowCustomPushPrompt( for message: ABKInAppMessage, notificationCenter: NotificationRequester = UNUserNotificationCenter.current() ) -> CustomPromptAction { + let featureSwitch = FeatureSwitch(rawValue: EnableImmediateNotificationRequest) + let shouldImmediatelyRequestNotificationAccess = ABTestCentral.isFeatureEnabled(featureSwitch) + guard shouldImmediatelyRequestNotificationAccess == false else { return .shouldNotShow } guard let dictionary = message.extras else { return .ignore } let key = "custom_push_prompt" guard let isCustomPushPromptMessageString = dictionary[key] as? String, let isCustomPushPromptMessage = Bool(isCustomPushPromptMessageString), isCustomPushPromptMessage else { return .ignore } return hasShownPushPrompt(for: notificationCenter) ? .shouldNotShow : .shouldShow } 39 — @basthomas

Slide 44

Slide 44 text

✅ 40 — @basthomas

Slide 45

Slide 45 text

TAKEAWAYS 41 — @basthomas

Slide 46

Slide 46 text

TAKEAWAYS ▸ Testing is hard, but never impossible 41 — @basthomas

Slide 47

Slide 47 text

TAKEAWAYS ▸ Testing is hard, but never impossible ▸ Writing testable code helps with more than tests 41 — @basthomas

Slide 48

Slide 48 text

TAKEAWAYS ▸ Testing is hard, but never impossible ▸ Writing testable code helps with more than tests ▸ Protocols and default expressions are ok 41 — @basthomas

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

THANKS! @BASTHOMAS 42 — @basthomas