Objective-C Singleton Pattern Updated For Testability

Posted by on Jan 24, 2013 in BDD, iOS, iPad, iPhone, Mac OS X, Software Development, TDD | 11 Comments

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.

11 Comments

  1. Jasper Blues
    January 24, 2013

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

    Reply
  2. Stepan
    April 3, 2013

    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;
    }
    }
    }

    Reply
    • Todd Huss
      April 3, 2013

      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.

      Reply
  3. Rohin Knight
    September 10, 2013

    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;
    }
    }

    Reply
  4. Brandon
    January 29, 2014

    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?

    Reply
  5. Christian
    April 24, 2014

    Thanks for the code. I got something wrong and couldn’t figure out what it was until I read your working example.

    Reply
  6. mauli
    August 14, 2014

    very useful blog

    Reply
  7. Joel
    October 25, 2014

    I like the way you think! And thanks so much for your blog, I enjoy it.

    But this doesn’t seem thread safe:

    @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

    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.

    Reply
    • Christopher Pickslay
      October 27, 2014

      Joel, 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!

      Reply

Leave a Reply