Objective-C Singleton Pattern Updated For Testability

SingletonAt 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:

// we can replace it with a mock object
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:

@implementation ArticleManager

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:

+(instancetype)sharedInstance {
    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:

+(void)setSharedInstance:(ArticleManager *)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:

once_token = 0;

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:

dispatch_once(&once_token, ^{
    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.

8 thoughts on “Objective-C Singleton Pattern Updated For Testability

  1. Nice 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).

  2. I 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;
    }
    }
    }

    • There’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.

  3. Could 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;
    }
    }

  4. I 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?

Leave a Reply

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

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>