Objective-C Singleton Pattern Updated For Testability
At Two Bit Labs we do a fair amount of unit testing. In places where we use singletons we use a variation on the the Objective-C dispatch_once pattern of thread safe singleton creation. This variation supports resetting the singleton or replacing it with a mock object.
That way in our unit tests we can do the following:
id mockManager = [OCMockObject mockForClass:[ArticleManager class]];
[ArticleManager setSharedInstance:mockManager];
// we can reset it so that it returns the actual ArticleManager
[ArticleManager setSharedInstance:nil];
Here’s what the pattern looks like:
static ArticleManager *_sharedInstance = nil;
static dispatch_once_t once_token = 0;
+(instancetype)sharedInstance {
dispatch_once(&once_token, ^{
if (_sharedInstance == nil) {
_sharedInstance = [[ArticleManager alloc] init];
}
});
return _sharedInstance;
}
+(void)setSharedInstance:(ArticleManager *)instance {
once_token = 0; // resets the once_token so dispatch_once will run again
_sharedInstance = instance;
}
@end
Breaking it down
Let’s break that down a bit. Here’s what the standard singleton pattern looks like in Objective-C:
static dispatch_once_t once_token;
static id sharedInstance;
dispatch_once(&once_token, ^{
sharedInstance = [[ArticleManager alloc] init];
});
return sharedInstance;
}
First we add a mutator that allows us to override the singleton with a test instance:
_sharedInstance = instance;
}
At the end of the test case, we reset the sharedInstance
to nil to prevent setup state from leaking from one test into another.
Now the problem is that once the singleton has been assigned to nil, the constructor will never run again, because once_token
has been assigned. We can fix that by resetting the predicate in the mutator:
Unfortunately, now when sharedInstance
is invoked in the code under test, it will overwrite the test singleton instance with a real instance, and the test will fail. So we add a nil check inside the dispatch_once
test:
if (_sharedInstance == nil) {
_sharedInstance = [[ArticleManager alloc] init];
}
});
This way, even if the dispatch_once predicate test fails, the constructor won’t initialize the instance variable if it’s already assigned.
11 Comments
Jasper Blues
January 24, 2013Nice work.
Do you have classes express their dependency on a shared instance or make them internally? I think the former case is better because it gives a clear contract, and there’s no need to go peeking inside the class. (glass-box vs black box testing).
Stepan
April 3, 2013I would make it even cleaner this way:
+(ArticleManager *)sharedInstance {
dispatch_once(&once_token, ^{
_sharedInstance = [[ArticleManager alloc] init];
});
return _sharedInstance;
}
+(void)setSharedInstance:(ArticleManager *)instance {
if (_sharedInstance != instance) {
_sharedInstance = instance;
if (!_sharedInstance) {
once_token = 0;
}
}
}
Todd Huss
April 3, 2013There’s a bug with that approach. If the first unit test sets the shared instance to mockArticleManager, once_token will still be 0, so the first call to get the sharedInstance will replace mockArticleManager with a real ArticleManager.
Rohin Knight
September 10, 2013Could you use @synchronized instead of dispatch_once?
E.g.
static ArticleManager* _sharedInstance = nil;
+ (AccountFactory*) sharedInstance
{
@synchronized(_sharedInstance) {
if (_sharedInstance == nil)
_sharedInstance = [[ArticleManager alloc] init];
return _sharedInstance;
}
}
+ (void)setSharedInstance:(ArticleManager *)instance {
@synchronized(_sharedInstance) {
_sharedInstance = instance;
}
}
Rohin Knight
September 10, 2013The only difference I’ve found so far is dispatch_once is a lot faster: http://bjhomer.blogspot.co.nz/2011/09/synchronized-vs-dispatchonce.html
Ben Blaukopf
September 22, 2016If you do that, then two threads calling sharedInstance simultaneously can both synchronize on nil, which is the same as no synchronisation. So there is a race condition at initial setup.
Brandon
January 29, 2014I actually do this because there are some cases where I need to reset my singleton.
I created a resetInstance method that will set the dispatch to 0 and sets the shared instance to nil.
Is there anything wrong with that approach?
Christian
April 24, 2014Thanks for the code. I got something wrong and couldn’t figure out what it was until I read your working example.
mauli
August 14, 2014very useful blog
Joel
October 25, 2014I like the way you think! And thanks so much for your blog, I enjoy it.
But this doesn’t seem thread safe:
static ArticleManager *_sharedInstance = nil;
static dispatch_once_t once_token = 0;
+(instancetype)sharedInstance {
dispatch_once(&once_token, ^{
if (_sharedInstance == nil) {
_sharedInstance = [[ArticleManager alloc] init];
}
});
return _sharedInstance;
}
+(void)setSharedInstance:(ArticleManager *)instance {
once_token = 0; // resets the once_token so dispatch_once will run again
_sharedInstance = instance;
}
@end
What if the following sequence of events happens:
1. Thread1 calls setSharedInstance with a non-nil instance arg with value V1.
2. Thread1 executes the first line of setSharedInstance to set once_token to 0.
3. Thread1 is paused by the OS.
4. Thread2 invokes the getter sharedInstance and runs to completion, setting _sharedInstance to value V2.
5. The caller in Thread2 thinks that V2 is the singleton shared instance.
6. The OS resumes Thread1 and executes the second line of setSharedInstance. This sets _sharedInstance to V1, overwriting V2.
7. So now Thread1 thinks the singleton has shared instance V1 while Thread2 thinks it’s V2.
8. Possible shenanigans ensue … 🙂
I think you need to use @synchronized or otherwise add thread safety. The virtue of the dispatch_once pattern is that only the single sharedInstance getter modifies _sharedInstance and it will execute only once due to the token. That is thread safe. As soon as you split it out into both a getter sharedInstance and setter setSharedInstance, that thread safety is lost.
If you’re only testing on one thread, it may not matter. But the thread safe dispatch_once singleton pattern is no longer thread safe …
Or am I missing something? 🙂
Thanks again.
Christopher Pickslay
October 27, 2014Joel, yes I think what you suggest could possibly happen. But it isn’t something we worry about, because the only reason
setSharedInstance:
exists is to be called in tests to inject a mock. It is never used in production code. I don’t love having code outside the tests that’s test-specific, but it solves a lot of testability problems for us, so we accept the tradeoff for now. Here’s hoping Swift enables some better patterns!