In this article, we will see how we can test code that interacts with the NotificationCenter. The Observer pattern is one of the first design patterns introduced on the famous Gang of Four book. It is a pattern that Apple uses in UIKit to notify us about events. The pattern is very useful because it decouples the sender from the receiver but overusing it can lead to difficulties to debug bugs. I use it only to subscribe to UIKit events or when I develop a framework and I have to notify the framework users.
Let’s start with our sample code. In this example we have a viewController that subscribes for “WeatherUpdate” events on viewDidLoad and unsubscribes from the event notification on the deinit method. When a “WeatherUpdate” event comes the updateWeatherForcast is being called:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
class ViewController: UIViewController { var weatherForcast = "" override func viewDidLoad() { super.viewDidLoad() NotificationCenter.default.addObserver(self, selector: #selector(updateWeatherForcast), name: Notification.Name(rawValue: "WeatherUpdate"), object: nil) } deinit { NotificationCenter.default.removeObserver(self) } @objc func updateWeatherForcast(notification : Notification) { guard notification.name == Notification.Name(rawValue: "WeatherUpdate")else { return } guard let userInfo = notification.userInfo else { return } guard let weatherForcast = userInfo["weatherForcast"] as? String else { return } self.weatherForcast = weatherForcast print(self.weatherForcast) } } |
The code in its current state, is not testable. So before we start writing our unit tests we have to bring the code in a testable state. We notice that we are calling the NotificationCenter in the viewDidLoad. Our object depends on a concrete implementation. Changing from NotificationCenter to another implementation is not straightforward. In this case, we have to stop thinking of concrete implementations but to think of what is the purpose of the NotificationCenter in our object. We are using it because we want our object to subscribe and unsubscribe from notifications. By having this in mind we can capture this intent a protocol:
1 2 3 4 5 6 |
protocol NotificationCenterObserverProtocol { func addObserver(_ observer: Any, selector aSelector: Selector, name aName: Notification.Name?, object anObject: Any?) func removeObserver(_ observer: Any) } extension NotificationCenter: NotificationCenterObserverProtocol {} |
The trick here is that we want our protocol to have the same method signatures as the NotificationCenter. Having the same method signatures makes the NotificationCenter to conform to this functionality efortless.
Now, all we have to do is to make our object depend on a NotificationCenterObserverProtocol instead of the NotificationCenter. Since it is a viewController, using constructor dependency injection can be tricky, we can use property injection. The object which creates the viewController can pass to it a concrete implementation of this protocol such as a NotificationCenter. Also since he don’t have control of the NotificationCenter we can replace it with a spy(mock) that confroms to the NotificationCenterObserverProtocol protocol.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
extension NotificationCenter: NotificationCenterObserverProtocol {} class ViewController: UIViewController { var notificationCenter: NotificationCenterObserverProtocol? = NotificationCenter.default //The object which is responsible for creating the viewController should set the NotificationCenter.default. var weatherForcast = "" override func viewDidLoad() { super.viewDidLoad() notificationCenter?.addObserver(self, selector: #selector(updateWeatherForcast), name: Notification.Name(rawValue: "WeatherUpdate"), object: nil) } deinit { notificationCenter?.removeObserver(self) } |
Having the code in a testable state, we start writing our unit tests. Let’s start by testing that the viewController subscribes for “WeatherUpdate” notifications. We can use a Spy to capture the addObserver method call:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
class ViewControllerTests: XCTestCase { func test_viewDidLoad_addObserverForWeatherUpdate() { let sut = ViewController() let spyNotificationCenter = SpyNotificationCenterObserver() sut.notificationCenter = spyNotificationCenter _ = sut.view XCTAssertEqual(Notification.Name("WeatherUpdate"), spyNotificationCenter.didAddObserverWithName) } } extension ViewControllerTests { class SpyNotificationCenterObserver: NotificationCenterObserverProtocol { var didAddObserverWithName : Notification.Name? var didCallRemoveObserver = false func addObserver(_ observer: Any, selector aSelector: Selector, name aName: Notification.Name?, object anObject: Any?) { didAddObserverWithName = aName } func removeObserver(_ observer: Any) { didCallRemoveObserver = true } } } |
Now, let’s test the removeObserver method call. We can use our spy here to capture the method call. Also, we can use the addTeardownBlock that it is called after the test has finished its execution.
1 2 3 4 5 6 7 8 9 10 11 12 |
func test_deinit_removeObserver() { let sut = ViewController() let spyNotificationCenter = SpyNotificationCenterObserver() sut.notificationCenter = spyNotificationCenter _ = sut.view addTeardownBlock { XCTAssertTrue(spyNotificationCenter.didCallRemoveObserver) } } |
We finished testing the subscribe and unsubscribe functionality. Now, we will test the method updateWeatherForcast. As we see the method has three guard clauses, so we have at least four paths to tests (3 from the guard clauses plus 1 ).
First let’s start with the happy path:
1 2 3 4 5 6 7 8 9 |
func test_updateWeatherForcast_updatesWeatherForcast() { let sut = ViewController() let notification = Notification(name: Notification.Name(rawValue: "WeatherUpdate"), object: nil, userInfo: ["weatherForcast" : "someForcast"]) sut.updateWeatherForcast(notification: notification) XCTAssertEqual("someForcast", sut.weatherForcast) } |
Now, let’s start with the first guard clause. We want to test that if the notification name is not “WeatherUpdate” the weatherForcast is not updated:
1 2 3 4 5 6 7 8 9 10 11 |
func test_updateWeatherForcast_doesNotUpdateWeatherForcast_whenNotificationIsNotNamedWeatherUpdate() { let sut = ViewController() let notification = Notification(name: Notification.Name(rawValue: "aNotifcation"), object: nil, userInfo: nil) sut.updateWeatherForcast(notification: notification) XCTAssertEqual("", sut.weatherForcast) } |
Now, we want to test the second guard clause. Let’s test that when the userInfo is nil the weatherForcast is not updated:
1 2 3 4 5 6 7 8 9 |
func test_updateWeatherForcast_doesNotUpdateWeatherForcast_whenUserInfoIsNil() { let sut = ViewController() let notification = Notification(name: Notification.Name(rawValue: "WeatherUpdate"), object: nil, userInfo: nil) sut.updateWeatherForcast(notification: notification) XCTAssertEqual("", sut.weatherForcast) } |
Now its the turn for the last guard clause. When the usrInfo does not contain the key “weatherForcast”, the weatherForcast is not updated:
1 2 3 4 5 6 7 8 9 |
func test_updateWeatherForcast_doesNotUpdateWeatherForcast_whenUserInfoDoesNotContainWeatherForcast() { let sut = ViewController() let notification = Notification(name: Notification.Name(rawValue: "WeatherUpdate"), object: nil, userInfo: ["someKey" : "someInfo"]) sut.updateWeatherForcast(notification: notification) XCTAssertEqual("", sut.weatherForcast) } |