Beliebte Suchanfragen

Cloud Native



Agile Methoden



Unit Testing Notification Observing on iOS

3.9.2014 | 4 minutes of reading time

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:

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

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

1- (void)testNotificationObserving
3    [_viewController viewWillAppear:NO];
4    // Check if observing
5    NLAssertObservingNotification(_viewController, @"notificationName", @"");
7    [_viewController viewWillDisappear:NO];
8    // Check if no longer observing
9    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:

1#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.

1static 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.

1void _swizzled_add_implementation(id self, SEL _cmd, id observer, SEL selector, NSString *name, id notiObj)
3    // check if dictionary of observed notifications exists
4    NSMutableDictionary *notifications = objc_getAssociatedObject(observer, kAssociatedNotificationsKey);
5    if (!notifications)
6    {
7        // if it doesn't create it and associate it
8        notifications = [NSMutableDictionary dictionary];
9        objc_setAssociatedObject(observer, kAssociatedNotificationsKey, notifications, OBJC_ASSOCIATION_RETAIN);
10    }
11    // set selector name for each notification
12    [notifications setObject:NSStringFromSelector(selector) forKey:name];
14    // Call original implementation
15    ((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:

1- (void)setUp
3    [super setUp];
5    Method originalAddMethod = class_getInstanceMethod([NSNotificationCenter class], @selector(addObserver:selector:name:object:));
6    _original_add_implementation = method_setImplementation(originalAddMethod, (IMP)_swizzled_add_implementation);
9- (void)tearDown
11    Method swizzledAddMethod = class_getInstanceMethod([NSNotificationCenter class], @selector(addObserver:selector:name:object:));
12    method_setImplementation(swizzledAddMethod, (IMP)_original_add_implementation);
14    [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:

1#define NLAssertObservingNotification(a1, notification, description, ...) \
2    do { \
3        @try { \
4            id _a1 = objc_getAssociatedObject(a1, kAssociatedNotificationsKey); \
5            id _val = [_a1 valueForKey:notification]; \
6            if (_a1 == nil || _val == nil) { \
7                if (description.length == 0) { \
8                    NSString *_obj = [NSString stringWithUTF8String:#a1]; \
9                    XCTFail(@"(%@) observing (%@)", _obj, notification); \
10                } else { \
11                    XCTFail(description, ##__VA_ARGS__); \
12                } \
13            } \
14        } \
15        @catch (id anException) { \
16            XCTFail(@"(%s) observing notification fails", #a1); \
17        } \
18    } while(0)

share post




Gemeinsam bessere Projekte umsetzen.

Wir helfen deinem Unternehmen.

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.