Overview

Unit Testing Notification Observing on iOS

11 Comments

While doing TDD I’ve often found that I needed to make sure that an object is subscribing to and unsubscribing from notifications at the right time. NSNotificationCenter has no publicly accessible observer index, so testing this is not as simple as checking if an object belongs to a collection.

As the proverb goes “There are more ways than one to skin a cat”. The same applies to testing a piece of code. A classic approach for testing notification observing is one where a test subclass of an object is created, the expected method is overridden which sets a public flag when called, and finally in the test a notification is fired and the flag is checked. Not good, too many steps, too much additional code that also needs to be maintained.

Another approach would be to mock the notification center, which requires replacing the default notification center either by using DI or swizlling out the implementation of defaultCenter to return the mock. Not as bad, but still it requirers premeditation which is not always possible if different people are writing code and tests. Using this approach with existing code would require refactoring and also mock objects could possibly break more complex tests.

I wanted an easy to use solution, without mocking or subclassing, that works the same way the standard test case assertions do. What I came up with is a base test case class with these three macros:

NLAssertObservingNotification(obj, notification, description, ...);
NLAssertObservingNotificationWithSelector(obj, notification, selector, description, ...);
NLAssertNotObservingNotification(obj, notification, description, ...);

With this approach testing observers is as simple as subclassing NLBaseTests and:

- (void)testNotificationObserving
{
    [_viewController viewWillAppear:NO];
    // Check if observing
    NLAssertObservingNotification(_viewController, @"notificationName", @"");
 
    [_viewController viewWillDisappear:NO];
    // Check if no longer observing
    NLAssertNotObservingNotification(_viewController, @"notificationName", @"");
}

If that is all you need to know you can grab to code over on Github. If you would like to know how this approach works continue reading.

The idea is that we inject code before all add and remove calls to the default NSNotificationCenter. That code will store all notification and selector names in a dictionary that is associated with the observer, and remove them from the dictionary when the observer is removed. This way the assertion really is as simple as checking if an object is part of a collection.

The two things that make this approach work are method swizzling and object association. So first thing we need to do is:

#import <objc/objc-runtime.h>

Next, we are going to replace the “addObserver:selector:name:object:” with our own implementation. But first we need a variable to keep track of the original implementation, since we’re going to need it later.

static IMP _original_add_implementation;

Our implementation of the addObserver method will fetch the associated dictionary from the observer or create it if it doesn’t exist and associate it with the object, and store the selector name as the value with the notification name as the key.

void _swizzled_add_implementation(id self, SEL _cmd, id observer, SEL selector, NSString *name, id notiObj)
{
    // check if dictionary of observed notifications exists
    NSMutableDictionary *notifications = objc_getAssociatedObject(observer, kAssociatedNotificationsKey);
    if (!notifications)
    {
        // if it doesn't create it and associate it
        notifications = [NSMutableDictionary dictionary];
        objc_setAssociatedObject(observer, kAssociatedNotificationsKey, notifications, OBJC_ASSOCIATION_RETAIN);
    }
    // set selector name for each notification
    [notifications setObject:NSStringFromSelector(selector) forKey:name];
 
    // Call original implementation
    ((void(*)(id,SEL,id,SEL,NSString*,id))_original_add_implementation)(self, _cmd, observer, selector, name, notiObj);
}

On the last line we call the original implementation of addObserver so we don’t break anything and the notification center works as intended. The long spaghetti cast before _original_add_implementation is needed so that ARC understands what it is supposed to do.

Since swizling these implementations is tedious work and easily forgettable we do it in the setup and tear down methods of the base class:

- (void)setUp
{
    [super setUp];
 
    Method originalAddMethod = class_getInstanceMethod([NSNotificationCenter class], @selector(addObserver:selector:name:object:));
    _original_add_implementation = method_setImplementation(originalAddMethod, (IMP)_swizzled_add_implementation);
}
 
- (void)tearDown
{
    Method swizzledAddMethod = class_getInstanceMethod([NSNotificationCenter class], @selector(addObserver:selector:name:object:));
    method_setImplementation(swizzledAddMethod, (IMP)_original_add_implementation);
 
    [super tearDown];
}

In the teardown we return the original implementation because we are good citizens and otherwise the test will get stuck in an infinite loop and crash.

The last thing we need is a way of getting the associated dictionary and checking if it contains a notification:

#define NLAssertObservingNotification(a1, notification, description, ...) \
    do { \
        @try { \
            id _a1 = objc_getAssociatedObject(a1, kAssociatedNotificationsKey); \
            id _val = [_a1 valueForKey:notification]; \
            if (_a1 == nil || _val == nil) { \
                if (description.length == 0) { \
                    NSString *_obj = [NSString stringWithUTF8String:#a1]; \
                    XCTFail(@"(%@) observing (%@)", _obj, notification); \
                } else { \
                    XCTFail(description, ##__VA_ARGS__); \
                } \
            } \
        } \
        @catch (id anException) { \
            XCTFail(@"(%s) observing notification fails", #a1); \
        } \
    } while(0)

Why a macro and not a function? With a function each failing test would fail in the base class and you would have a hard time tracking where the failure occurred. With a macro the test fails inline and you can jump straight to it. Also, Apple does it the same way.

This completes the addition part of the solution, the same principle applies to removal with removeObserver:name:object: and removeObserver:. You can check out the complete implementation on Github.

Have any ideas, suggestions or questions? Hit the comments section bellow or contact me @nlajic on Twitter.

Kommentare

  • peter

    18. March 2015 von peter

    Very interesting , tried the code and it works.
    Do you have a swift version available? would be greatly appreciated

    • Nikola Lajic

      18. March 2015 von Nikola Lajic

      Hi Peter,

      I have updated the code on GitHub. Adding it to the project is a bit more complicated, so check out the readme. Let me know if you run in to any problems.

      • peter

        18. March 2015 von peter

        Follow your instructions so copied .h and .m into test target
        Bridge file is setup correctly
        even included NLBaseTests.siwft extension but i get error when compiling test target
        use on unresolved identifier Obj and NLAssertObservingNotificationWithSelector

        please advice

        • Nikola Lajic

          18. March 2015 von Nikola Lajic

          That happens when the “Objective-C Bridging Header” path is not set correctly.
          Set the path in the test target build settings. Make sure that it is only set for the test target and not also for the app target or the whole project because it will cause other issues.
          The path should look similar to this $(SRCROOT)/SwiftSwizzlingTests/SwiftSwizzlingTests-Bridging-Header.h

          • peter

            18. March 2015 von peter

            Path is set correctly but still doesn’t work
            Error is use of unresolved ‘NLAssetObservingNotification’

          • peter

            18. March 2015 von peter

            Path is set correctly but still doesn’t work
            Error is use of unresolved ‘NLAssetObservingNotification’
            dont understand why

          • Nikola Lajic

            18. March 2015 von Nikola Lajic

            Please check the Example-Swift project on the GitHub repo and compare.

          • peter

            18. March 2015 von peter

            it is the same .h.m and .swift are there. Bridging Header file is set correctly. NLBaseTests.h is imported in the bridging file. All cleaned and rebuild. File are linked to the test target but still no luck

          • Nikola Lajic

            18. March 2015 von Nikola Lajic

            Try to add it to a small sample project to see if you’ve missed something. The only way I was able to reproduce this error is if the bridging file is not importing the needed header or if the bridging file path is wrong.

          • peter

            18. March 2015 von peter

            Found out was wrong, my tests class was of course subclassing XCTest and not NLBaseTests
            Saw you made a swift extension as a subclass of NLTests which is confusing, in swift extensions should extend XCTest in this case, that would be cleaner if you see what i mean.
            Yet still very good solution and thanks for good response

          • Nikola Lajic

            18. March 2015 von Nikola Lajic

            I’m glad you figured it out. The extension is just a quick fix to get it working with swift, if I made a fully swift solution then the extension might be on XCTest.

Comment

Your email address will not be published. Required fields are marked *