views:

316

answers:

3

For a game I'm developing, I have several model classes that trigger notifications when their state changes. Then, the view subscribes to those notifications and can react on them.

I'm doing my unit tests for the model with OCUnit, and want to assert that the expected notifications were posted. For that, I'm doing something like this:

- (void)testSomething {
    [[NSNotificationCenter defaultCenter] addObserver:notifications selector:@selector(addObject:) name:kNotificationMoved object:board];

    Board *board = [[Board alloc] init];
    Tile *tile = [Tile newTile];

    [board addTile:tile];

    [board move:tile];

    STAssertEquals((NSUInteger)1, [notifications count], nil);
    // Assert the contents of the userInfo as well here

    [board release];
}

The idea is that the NSNotificationCenter will add the notifications to the NSMutableArray by calling its addObject: method.

When I run it, however, I see that addObject: is being sent to some other object (not my NSMutableArray) causing OCUnit to stop working. However, if I comment out some code (such as the release calls, or add a new unit test) everything starts working as expected.

I'm assuming this has to o with a timing issue, or NSNotificationCenter relying on the run loop in some way.

Is there any recommendation to test this? I know I could add a setter in Board and inject my own NSNotificationCenter, but I'm looking for a quicker way to do it (maybe some trick on how to replace the NSNotificationCenter dynamically).

A: 

There are no timing issues or runloop related problems since everything in your code is non-concurrent and should be executed immediately. NSNotificationCenter only postpones notification delivery if you use an NSNotificationQueue.

I think everything is correct in the snippet you posted. Maybe there's an issue with the mutable array 'notifications'. Did you init and retain it correctly? Try to add some object manually instead of using the notification trick.

Nikolai Ruhe
I'm allocating the array with [NSMutableArray arrayWithCapacity]. I'm not retaining it (it's a local variable, so the NSAutoReleasePool won't release it yet).
pgb
Found my problem. I'm not removing the observer from the NSNotificationCenter, so when a second test runs it tries to notify an object that no longer exists in the heap.
pgb
A: 

If you suspect your tests have timing issues - you may want to consider injecting your own notification mechanism into your board object (which is probably just a wrapper of the existing apple version).

That is:

Board *board = [[Board alloc] initWithNotifier: someOtherNotifierConformingToAProtocol];

Presumably your board object posts some notification - you would use your injected notifier in that code:

-(void) someBoardMethod {

  // ....

  // Send your notification indirectly through your object
  [myNotifier pushUpdateNotification: myAttribute];
}

In your test - you now have a level of indirection that you can use for testing, so you can implement a test class the conforms to your AProtocol - and maybe counts up the pushUpdateNotification: calls. In your real code you encapsulate the code you probably already have in Board that does the notification.

This of course is a classic example of where MockObjects are useful - and there is OCMock which well let you do this without having to have a test class to do the counting (see: http://www.mulle-kybernetik.com/software/OCMock/)

your test would problably have a line something like:

[[myMockNotifer expect] pushUpdateNotification: someAttribute];

Alternatively you could consider using a delegate instead of notifications. There is a good pro/con set of slides here: http://www.slideshare.net/360conferences/nsnotificationcenter-vs-appdelegate.

TimM
I think notifications are fine for this case, since I trigger animations on the View layer asynchronously. I was trying to avoid injecting my custom notification class, as to simplify the testing code and the main class, but it seems to be the only choice so far.
pgb
A: 

Found the problem. When testing notifications you need to remove the observer after you have tested it. Working code:

- (void)testSomething {
    [[NSNotificationCenter defaultCenter] addObserver:notifications selector:@selector(addObject:) name:kNotificationMoved object:board];

    Board *board = [[Board alloc] init];
    Tile *tile = [Tile newTile];

    [board addTile:tile];

    [board move:tile];

    STAssertEquals((NSUInteger)1, [notifications count], nil);
    // Assert the contents of the userInfo as well here

    [board release];
    [[NSNotificationCenter defaultCenter] removeObserver:notifications name:kNotificationMoved object:board];
}

If you fail to remove the observer, after a test runs and some local variables are released, the notification center will try to notify those old objects when running any subsequent test that triggers the same notification.

pgb