In what situation would one use expectationForNotification in swift testing

As you have already imagined expectationForNotification is a convenience expectation for checking if a notification was raised.

This test:

func testItShouldRaiseAPassNotificationV1() {
    let expectation = expectationWithDescription("Notification Raised")
    let sub = NSNotificationCenter.defaultCenter().addObserverForName("evPassed", object: nil, queue: nil) { (not) -> Void in
        expectation.fulfill()
    }
    NSNotificationCenter.defaultCenter().postNotificationName("evPassed", object: nil)
    waitForExpectationsWithTimeout(0.1, handler: nil)
    NSNotificationCenter.defaultCenter().removeObserver(sub)
}

can be replaced by this one:

func testItShouldRaiseAPassNotificationV2() {
    expectationForNotification("evPassed", object: nil, handler: nil)
    NSNotificationCenter.defaultCenter().postNotificationName("evPassed", object: nil)
    waitForExpectationsWithTimeout(0.1, handler: nil)
}

You can find a good explanation in this Objc.io number.


In order to understand the difference between expectation(forNotification:, object:, handler:) and expectation(description:), I have build a simple XCTestCase subclass with Swift 3.

Here, we want to test that a BlockOperation that posts a Notification updates a specified Int? property of our class with the requested value of 50.


1. Using expectation(description:) with addObserver(_:, selector:, name:, object:)

import XCTest

class AppTests: XCTestCase {

    var testExpectation: XCTestExpectation?
    var finalAmount: Int?

    func testFinalAmount() {
        let notificationName = Notification.Name(rawValue: "BlockNotification")

        // Set self as an observer
        let selector = #selector(updateFrom(notification:))
        NotificationCenter.default.addObserver(self, selector: selector, name: notificationName, object: nil)

        // Set expectation
        testExpectation = expectation(description: "Did finish operation expectation")

        // Set and launch operation block and wait for expectations
        let operation = BlockOperation(block: {
            NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["amount": 50])
        })
        operation.start()
        waitForExpectations(timeout: 3, handler: nil)

        // Asserts
        XCTAssertNotNil(finalAmount)
        XCTAssertEqual(finalAmount, 50)
    }

    func updateFrom(notification: Notification) {
        if let amount = notification.userInfo?["amount"] as? Int {
            self.finalAmount = amount
        }
        self.testExpectation?.fulfill()
    }

}

2. Using expectation(description:) with addObserver(forName:, object:, queue:, using:)

import XCTest

class AppTests: XCTestCase {

    var finalAmount: Int?

    func testFinalAmount() {
        let notificationName = Notification.Name(rawValue: "BlockNotification")

        // Set expectation
        let testExpectation = expectation(description: "Did finish operation expectation")

        // Set self as an observer
        let handler = { (notification: Notification) -> Void in
            if let amount = notification.userInfo?["amount"] as? Int {
                self.finalAmount = amount
            }
            testExpectation.fulfill()
        }
        NotificationCenter.default.addObserver(forName: notificationName, object: nil, queue: nil, using: handler)

        // Set and launch operation block and wait for expectations
        let operation = BlockOperation(block: {
            NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["amount": 50])
        })
        operation.start()
        waitForExpectations(timeout: 3, handler: nil)

        // Asserts
        XCTAssertNotNil(finalAmount)
        XCTAssertEqual(finalAmount, 50)
    }

}

3. Using expectation(forNotification:, object:, handler:)

import XCTest

class AppTests: XCTestCase {

    var finalAmount: Int?

    func testFinalAmount() {
        let notificationName = Notification.Name(rawValue: "BlockNotification")

        // Set expectation
        let handler = { (notification: Notification) -> Bool in
            if let amount = notification.userInfo?["amount"] as? Int {
                self.finalAmount = amount
            }
            return true
        }
        expectation(forNotification: notificationName.rawValue, object: nil, handler: handler)

        // Set and launch operation block and wait for expectations
        let operation = BlockOperation(block: {
            NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["amount": 50])
        })
        operation.start()
        waitForExpectations(timeout: 3, handler: nil)

        // Asserts
        XCTAssertNotNil(finalAmount)
        XCTAssertEqual(finalAmount, 50)
    }

}

tl;dr

Using expectation(forNotification: String, object:, handler:) instead of expectation(description:) in our test case provides some advantages:

  • our test now requires less lines of code (31 instead of 35 or 37 lines),
  • our test does not require anymore to use addObserver(_:, selector:, name:, object:) with a #selector or addObserver(forName:, object:, queue:, using:),
  • our test does not require anymore to declare an XCTestExpectation instance as a property of our class or as a scoped variable of our test method and to mark it as having been met at some point with fulfill().

You should not depend on UIKit's NotificationCenter. Make a boundary of your type and test only if your type is sending the command to the right object. Here is an example of how you can make NotificationCenter adopts your code. (I can't access Xcode right now, so it may have some typo)

protocol NotificationCenterProtocol {
  func post(notification: Notification)
}

extension NotificationCenter: NotificationCenterProtocol {}

class SpyNotificationCenter: NotificationCenterProtocol {
  var didPostNotification = false

  func post(notification: Notification) {
    didPostNotification = true
  }

}