Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unit testing iOS 10 notifications

In my app I wish to assert that notifications have been added in the correct format. I'd normally do this with dependency injection, but I can't think of a way to test the new UNUserNotificationCenter API.

I started to create a mock object which would capture the notification request:

import Foundation
import UserNotifications

class NotificationCenterMock: UNUserNotificationCenter {
    var request: UNNotificationRequest? = nil
    override func add(_ request: UNNotificationRequest, withCompletionHandler completionHandler: ((Error?) -> Void)? = nil) {
        self.request = request
    }
}

However, UNUserNotificationCenter has no accessible initializers I can't instantiate the mock.

I'm not even sure I can test by adding the notification request and fetching the current notifications, as the tests would need to request permission on the Simulator which would stall the tests. Currently I've refactored the notification logic into a wrapper, so I can at least mock that throughout my application and manually test.

Do I have any better options than manual testing?

like image 610
squarefrog Avatar asked Nov 29 '16 09:11

squarefrog


Video Answer


3 Answers

You can create a protocol for the methods you are using, and make an extension on UNUserNotificationCenter to conform to it. This protocol would act as a "bridge" between the original UNUserNotificationCenter implementation and your mock object to replace its method implementations.

Here's an example code I wrote in a playground, and works fine:

/* UNUserNotificationCenterProtocol.swift */

// This protocol allows you to use UNUserNotificationCenter, and replace the implementation of its 
// methods in you test classes.
protocol UNUserNotificationCenterProtocol: class {
  // Declare only the methods that you'll be using.
  func add(_ request: UNNotificationRequest,
           withCompletionHandler completionHandler: ((Error?) -> Void)?)
}

// The mock class that you'll be using for your test classes. Replace the method contents with your mock
// objects.
class MockNotificationCenter: UNUserNotificationCenterProtocol {

  var addRequestExpectation: XCTestExpectation?

  func add(_ request: UNNotificationRequest,
           withCompletionHandler completionHandler: ((Error?) -> Void)?) {
    // Do anything you want here for your tests, fulfill the expectation to pass the test.
    addRequestExpectation?.fulfill()
    print("Mock center log")
    completionHandler?(nil)
  }
}

// Must extend UNUserNotificationCenter to conform to this protocol in order to use it in your class.
extension UNUserNotificationCenter: UNUserNotificationCenterProtocol {
// I'm only adding this implementation to show a log message in this example. In order to use the original implementation, don't add it here.
  func add(_ request: UNNotificationRequest, withCompletionHandler completionHandler: ((Error?) -> Void)?) {
    print("Notification center log")
    completionHandler?(nil)
  }
}

/* ExampleClass.swift */

class ExampleClass {

  // Even though the type is UNUserNotificationCenterProtocol, it will take UNUserNotificationCenter type
  // because of the extension above.
  var notificationCenter: UNUserNotificationCenterProtocol = UNUserNotificationCenter.current()

  func doSomething() {
    // Create a request.
    let content = UNNotificationContent()
    let request = UNNotificationRequest(identifier: "Request",
                                           content: content,
                                           trigger: nil)
    notificationCenter.add(request) { (error: Error?) in
      // completion handler code
    }
  }
}

let exampleClass = ExampleClass()
exampleClass.doSomething() // This should log "Notification center log"

EDITED:
/* TestClass.Swift (unit test class) */

class TestClass {
  // Class being tested 
  var exampleClass: ExampleClass!    
  // Create your mock class.
  var mockNotificationCenter = MockNotificationCenter()

  func setUp() {
     super.setUp()
     exampleClass = ExampleClass()
     exampleClass.notificationCenter = mockNotificationCenter 
  }

  func testDoSomething() {
    mockNotificationCenter.addRequestExpectation = expectation(description: "Add request should've been called")
    exampleClass.doSomething()
    waitForExpectations(timeout: 1)
  }
}
// Once you run the test, the expectation will be called and "Mock Center Log" will be printed

Keep in mind that every time you use a new method, you'll have to add it to the protocol, or the compiler will complain.

Hope this helps!

like image 159
franciscojma86 Avatar answered Sep 17 '22 12:09

franciscojma86


You can utilize UNUserNotificationCenter, then setValue on the returned settings

UNUserNotificationCenter.current().getNotificationSettings(completionHandler: { settings in
    let status: UNAuthorizationStatus = .authorized
    settings.setValue(status.rawValue, forKey: "authorizationStatus")
    completionHandler(settings)
})
like image 22
onmyway133 Avatar answered Sep 19 '22 12:09

onmyway133


I managed to mock UNNotificationSettings using below

let object = UIView()
let data = try! NSKeyedArchiver.archivedData(withRootObject: object, requiringSecureCoding: false)
let coder = try! NSKeyedUnarchiver(forReadingFrom: data)
let setting = UNNotificationSettings(coder: coder)
setting?.setValue(UNAuthorizationStatus.notDetermined.rawValue, forKey: "authorizationStatus")
like image 32
Deepika Avatar answered Sep 20 '22 12:09

Deepika