27

I am working in swift, I want to refresh a page so I am sending it using notification, I am posting a notification in one ViewController and adding observer in another and it is working perfectly. What I want to do is add unit test to it in swift. I checked many sites but was not able to do it. I am new to swift and don't know where to start.

Basically the working is, when i click the button notification is posted and when the next view controller is loaded the notification observer is added.

How can I do the unit testing

Thanks in advance

Edit: Code

NSNotificationCenter.defaultCenter().postNotificationName("notificationName", object: nil)

and adding observer as

NSNotificationCenter.defaultCenter().addObserver(self, selector: "vvv:",name:"notificationName", object: nil)
user2413621
  • 2,916
  • 7
  • 24
  • 28

3 Answers3

40

XCTest has a class specifically for testing Notifications: XCTNSNotificationExpectation. You create one of these expectations, and it's fulfilled when a notification is received. You'd use it like:

// MyClass.swift
extension Notification.Name {
    static var MyNotification = Notification.Name("com.MyCompany.MyApp.MyNotification")
}

class MyClass {
    func sendNotification() {
        NotificationCenter.default.post(name: .MyNotification,
                                      object: self,
                                    userInfo: nil)
    }
}


// MyClassTests.swift
class MyClassTests: XCTestCase {
    let classUnderTest = MyClass()

    func testNotification() {
        let notificationExpectation = expectation(forNotification: .MyNotification, 
                                                           object: classUnderTest, 
                                                          handler: nil)

        classUnderTest.sendNotification()

        waitForExpectations(timeout: 5, handler: nil)
    }
}

XCTestCase's expectation(forNotification:object:handler:) is a convenience method to create an instance of XCTNSNotificationExpectation, but for more control, you could instantiate one and configure it yourself. See the docs.

zpasternack
  • 17,838
  • 2
  • 63
  • 81
  • 7
    one clarification is that in `expectation`, the `object` generally is not the "class under test" (i.e. not the class that sends notification), but the object sent within notification. In this particular example they are the same, but usually they are not. In OP example for instance, it's `nil`: `expectation(forNotification: .MyNotification, object: nil, handler: nil)` – timbre timbre Dec 17 '18 at 22:20
  • In the `sendNotification` method, make sure you are assigning `self` as the `object`. I had `nil` which causes the test to fail. – nivbp Dec 05 '22 at 12:20
18

The general solution is: Use dependency injection (DI) to make your components unit-testable. You can choose use a DI framework (I don't know if there is any good framework for Swift exists yet) or use native approach (i.e. pass object around)

One possible approach for your problem is to wrap NSNotificationCenter to make it mockable/injectable.

This is just a basic idea how you can decouple dependencies. Please don't just copy & paste the code below and expect it to work without understanding it.

import Foundation

protocol NotificationCenter {
    func postNotificationName(name: String, object: AnyObject?)

    // you can make it take the arguments as NSNotificationCenter.addObserver
    func addObserver(callback: AnyObject? -> Void)
}

class MyNotificationCenter : NotificationCenter {
    var _notificationCenter: NSNotificationCenter

    init(_ center: NSNotificationCenter) {
        _notificationCenter = center
    }

    func postNotificationName(name: String, object: AnyObject?) {
        // call NSNotificationCenter.postNotificationName
    }

    func addObserver(callback: AnyObject? -> Void) {
        // call NSNotificationCenter.addObserver
    }
}

class MockNotificationCenter : NotificationCenter {
    var postedNotifications: [(String, AnyObject?)] = []
    var observers: [AnyObject? -> Void] = []

    func postNotificationName(name: String, object: AnyObject?) {
        postedNotifications.append((name, object))
    }

    func addObserver(callback: AnyObject? -> Void) {
        observers.append(callback)
    }
}

class MyView {
    var notificationCenter: NotificationCenter

    init(notificationCenter: NotificationCenter) {
        self.notificationCenter = notificationCenter
    }

    func handleAction() {
        self.notificationCenter.postNotificationName("name", object: nil)
    }
}

class MyController {
    var notificationCenter: NotificationCenter

    init(notificationCenter: NotificationCenter) {
        self.notificationCenter = notificationCenter
    }

    func viewDidLoad() {
        self.notificationCenter.addObserver {
            println($0)
        }
    }
}

// production code
// in AppDeletate.applicationDidFinishLaunching
let notificationCenter = MyNotificationCenter(NSNotificationCenter.defaultCenter())

// pass it to your root view controller
let rootViewController = RootViewController(notificationCenter: notificationCenter)
// or
rootViewController.notificationCenter = notificationCenter

// in controller viewDidLoad
self.myView.notificationCenter = self.notificationCenter

// when you need to create controller
// pass notificationCenter to it
let controller = MyController(notificationCenter: notificationCenter)

// in unit test

func testMyView() {
    let notificationCenter = MockNotificationCenter()
    let myView = MyView(notificationCenter: notificationCenter)
    // do something with myView, assert correct notification is posted
    // by checking notificationCenter.postedNotifications
}

func testMyController() {
    let notificationCenter = MockNotificationCenter()
    let myController = MyController(notificationCenter: notificationCenter)
    // assert notificationCenter.observers is not empty
    // call it and assert correct action is performed
}
mfaani
  • 33,269
  • 19
  • 164
  • 293
Bryan Chen
  • 45,816
  • 18
  • 112
  • 143
  • i am new to swift, can u pls tell what is the basic idea behind the code working? – user2413621 Feb 03 '15 at 07:30
  • @user2413621 google and learn dependency injection. it is a general programming concept have nothing to do with Swift. – Bryan Chen Feb 03 '15 at 07:38
  • i am getting a error Type 'MyNotificationCenter' does not conform to protocol 'NotificationCenter' in line let notificationCenter = MyNotificationCenter(NSNotificationCenter.defaultCenter()) // somewhere else let myView = MyView(notificationCenter: notificationCenter) – user2413621 Feb 03 '15 at 09:20
  • @user2413621 you can check the count of the array – Bryan Chen Feb 04 '15 at 10:18
  • already done that thanks for the reply. I am stuck with another problem can u pls tell how to post and recieve notification, i mean with this code let notificationCenter = MyNotificationCenter(NSNotificationCenter.defaultCenter()) let myView = MyView(notificationCenter: notificationCenter) let controller = MyController(notificationCenter: notificationCenter) – user2413621 Feb 04 '15 at 11:29
  • A good link for Dependency injection: https://medium.com/ios-os-x-development/dependency-injection-in-swift-a959c6eee0ab#.ixf7agiae – mfaani Aug 11 '16 at 21:18
  • The first code snippet that contains the `NotificationCenter` are we to write that in a new `.swift` file that only targets on the Unit Tests? Or simply just write them into the TestClass itself? OR should we refractor our entire development code itself? Can explain the thought process? – mfaani Aug 12 '16 at 16:39
  • the `NotificationCenter ` protocol and an implementation (`MyNotificationCenter )` should be used by the app and then `MockNotificationCenter` is used only by the unit test target. – Bryan Chen Aug 13 '16 at 08:05
  • I don't believe I have came back and forth to a question this many times, probably 50 times by now in the past 10 days.Every 2-3 days I figure out something new. :). Shouldn't the `func addObserver(callback: AnyObject? -> Void)` instead be `func addObserver(callback: AnyObject? -> Void, name:String, object:AnyObject?)`. Also a stupid question: `MockNotificationCenter` class should that be **written** in production code or only inside the testclass itself? ( I know you already said it should be *used*, but still confused of where it should be written) – mfaani Aug 18 '16 at 21:37
  • 2
    @Honey you are right the method should have name and object parameter. `MockNotificationCenter` should be written in unit test code only. – Bryan Chen Aug 18 '16 at 22:31
  • Thank you so much for getting back, no one on our team knows how to do this, not even the tech lead... So what I am trying to do is I have an ErrorHandler class that is observes to a notificaiton that is sent from class1,class2,class3 and then pops an alert. If I understand correctly class1,2,3 are the equivalent of `MyView` in your example and a `var notificationCenter: NotificationCenter` should be added to each class and also be initialized while my ErrorHandlerClass is the equivalent of `MyController`. Right? – mfaani Aug 18 '16 at 22:55
  • @Honey Yes. Just make sure you have a single notification centre created and pass it around. Or use Singleton if passing objects is too hard. But avoid Singleton if possible. – Bryan Chen Aug 18 '16 at 23:29
  • I think, now I understand all the classes and all lines, except that I don't understand your thought process. Can you explain what is the **purpose** of having `MyNotificationCenter` & `MockNotificationCenter`? – mfaani Aug 19 '16 at 00:49
  • Use `MyNotificationCenter` for production. Use `MockNotificationCenter` for testing. In order to fully test `MyView`, you need to supply `MockNotificationCenter` in unit test. In production code, you supply `MyNotificationCenter` instead. – Bryan Chen Aug 19 '16 at 00:52
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/121299/discussion-between-honey-and-bryan-chen). – mfaani Aug 19 '16 at 00:56
  • Making the notification center a property in the view seems like a very odd solution. There is absolutely no need to record the received notifications in a property either, you can just register as an observer and ensure that the notification observation callback fired. – mz2 Oct 17 '16 at 16:38
0

Here is a simpler solution:

Step 1: Capture the notificationCenter object in an ambiant variable to be able to replace it with some spy class in your unit tests.

// In your production code:
var notificationCenter = NSNotificationCenter.defaultCenter()

// The code you are testing:
notificationCenter.postNotificationName("notificationName", object: nil)

Step 2: Define your spy class using inheritance to be able to detect whether the notification was posted or not.

// In your test code
private class NotificationCenterSpy: NotificationCenter {
    var notificationName: String?

    override func post(_ notificationName: String, object anObject: Any?) 
    {
        self.notificationName = aName
    }
}

Step 3: replace the ambiant variable in your unit test.

// In your test code:

// Given
// setup SUT as usual ...
let notificationCenterSpy = NotificationCenterSpy()
sut.notificationCenter = notificationCenterSpy

// When
sut.loadView()

// Then
XCTAssertEqual(notificationCenterSpy.notificationName, "notificationName")

Step 4: Testing the receiver View Controller

You should not test whether the receiver View Controller observes the change or not, you should test behaviour.

Something should be happening when the notification is received? That is what you should be testing, from your test code, post a notification and see if this behaviour happened (in your case if the page gets refreshed).

michael-martinez
  • 767
  • 6
  • 24