NSNotificationCenter defaultCenter is a gross implicit dependency in a lot of code. The reason it is so bad if you are writing tests is because of handling of notifications for things like UIApplicationDidBecomeActiveNotification, where the simulator is going to fire these notifications as it goes through the business of running the application you just launched, but your tests may be already in flight. That example is a little contrived because UIApplicationDidBecomeActiveNotification doesn't often fire during test runs, but let's just move past that.
I like creating and injecting a NSNotificationCenter into my objects under test in order to explicitly allow me to post notifications, test results, etc and not have to worry about the object in my actual running application getting an unexpected notification and to make sure that my object under test doesn't get notifications being posted from the running application. But sometimes you just need to mock the class method [NSNotificationCenter defaultCenter] for the duration of your test and you can get an error saying tests didn't finish when you do this. The solution to this problem is to make sure you call [mockCenter stopMocking] in your test tearDown.
@interface DTClassToTest : NSObject{ | |
id notificationObserver; | |
} | |
@property (strong, nonatomic) NSNotificationCenter *notificationCenter; | |
@property (assign, nonatomic) BOOL receivedNotification; | |
@end | |
@implementation DTClassToTest | |
-(id)init{ | |
return [self initWithNotificationCenter:[NSNotificationCenter defaultCenter]]; | |
} | |
-(id)initWithNotificationCenter:(NSNotificationCenter *)notificationCenter{ | |
if (!(self = [super init])) return self; | |
_notificationCenter = notificationCenter; | |
_receivedNotification = NO; | |
return self; | |
} | |
- (void)registerForNotifications{ | |
id __weak wself = self; | |
notificationObserver = [self.notificationCenter addObserverForName:@"CONTRIVED_NOTIFICATION" | |
object:nil | |
queue:[NSOperationQueue mainQueue] | |
usingBlock:^(NSNotification *n){ | |
DTClassToTest *self = wself; | |
self.receivedNotification = YES; | |
}]; | |
} | |
- (void)dealloc{ | |
[_notificationCenter removeObserver:notificationObserver]; | |
} | |
@end | |
@interface DTClassToTestTests : XCTestCase{ | |
DTClassToTest *objectUnderTest; | |
NSNotificationCenter *testNotificationCenter; | |
} | |
@end | |
// If you can inject notification center into a class for tests, then this is the best way to do this | |
@implementation DTClassToTestTests | |
- (void)setUp{ | |
[super setUp]; | |
testNotificationCenter = [NSNotificationCenter new]; | |
objectUnderTest = [[DTClassToTest alloc] initWithNotificationCenter:testNotificationCenter]; | |
} | |
- (void)test_responds_to_notification_correctly{ | |
[objectUnderTest registerForNotifications]; | |
[testNotificationCenter postNotificationName:@"CONTRIVED_NOTIFICATION" object:nil]; | |
XCTAssertTrue(objectUnderTest.receivedNotification); | |
} | |
@end | |
@interface DTClassToTestTestsTheClassMockWay : XCTestCase{ | |
DTClassToTest *objectUnderTest; | |
NSNotificationCenter *testNotificationCenter; | |
id mockCenter; | |
} | |
@end | |
// if there is no injection point, you can use OCMock to mock the defaultCenter class method. but you need to explicitly call | |
// stopMocking in your test tearDown. | |
@implementation DTClassToTestTestsTheClassMockWay | |
- (void)setUp{ | |
[super setUp]; | |
testNotificationCenter = [NSNotificationCenter new]; | |
mockCenter = [OCMockObject niceMockForClass:NSNotificationCenter.class]; | |
[[[[mockCenter stub] classMethod] andReturn:testNotificationCenter] defaultCenter]; | |
objectUnderTest = [DTClassToTest new]; // Will use [NSNotificationCenter defaultCenter]; | |
} | |
- (void)tearDown{ | |
[mockCenter stopMocking]; //if you omit this line in your tear down, your tests will not finish. | |
[super tearDown]; | |
} | |
- (void)test_responds_to_notification_correctly{ | |
[objectUnderTest registerForNotifications]; | |
[testNotificationCenter postNotificationName:@"CONTRIVED_NOTIFICATION" object:nil]; | |
XCTAssertTrue(objectUnderTest.receivedNotification); | |
} | |
@end |
Comments