Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Reset singleton instance to nil after each test case

I am using OCMock 3 to unit test my iOS project.

I use dispatch_once() created a singleton class MyManager :

@implementation MyManager

+ (id)sharedInstance {
    static MyManager *sharedMyManager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedMyManager = [[self alloc] init];
    });
    return sharedMyManager;
}

I have a method in School class which uses the above singleton:

@implementation School
...
- (void) createLecture {
  MyManager *mgr = [MyManager sharedInstance];
  [mgr checkLectures];
  ...
}
@end

Now, I want to unit test this method, I use a partial mock of MyManager:

- (void) testCreateLecture {
  // create a partially mocked instance of MyManager
  id partialMockMgr = [OCMockObject partialMockForObject:[MyManager sharedInstance]];

  // run method to test
  [schoolToTest createLecture];
  ...
}

- (void)tearDown {
  // I want to set the singleton instance to nil, how to?
  [super tearDown];
}

In tearDown phase, I want to set the singleton instance to nil so that the following test case could start from clean state.

I know on internet, some people suggest to move the static MyManager *sharedMyManager outside the +(id)sharedInstance method. But I would like to ask, is there any way to set the instance to nil without moving it outside +(id)sharedInstance method? (Any solution like java reflection?)

like image 591
Leem.fin Avatar asked May 31 '16 15:05

Leem.fin


People also ask

Can we Deinit a Singleton object?

If you have a regular object that you can't deinitialize it's a memory problem. Singletons are no different, except that you have to write a function to do it. Singletons have to be completely self managed. This means from init to deinit.

Can a Singleton be null?

the instance on a Singleton could never become null, except for when the app would be completely restarted (in which case it should by recreated by my application class).

How do you reset a Singleton object in Python?

Using this new definition of the singleton, you can write a clear method that can be called by any of the classes initialized with the Singleton metaclass and it will clear the _instance attribute. So in your case, MyClass. clear() would reset the _instance attribute to None.

How do you destroy a Singleton?

There is no way to destroy a Singleton without breaking the Singleton property. As others have said, maybe a Singleton isn't appropriate here. If the number of instances of the class can go down to zero, then it's not a Singleton.


2 Answers

You can't achieve what you want with a local static variable. Block-scoped statics are only visible inside their lexical context.

We do this by making the singleton instance a static variable scoped to the class implementation and adding a mutator to override it. Generally that mutator is only called by tests.

@implementation MyManager

static MyManager *_sharedInstance = nil;
static dispatch_once_t once_token = 0;

+(instancetype)sharedInstance {
    dispatch_once(&once_token, ^{
        if (_sharedInstance == nil) {
            _sharedInstance = [[MyManager alloc] init];
        }
    });
    return _sharedInstance;
}

+(void)setSharedInstance:(MyManager *)instance {
    once_token = 0; // resets the once_token so dispatch_once will run again
    _sharedInstance = instance;
}

@end

Then in your unit test:

// we can replace it with a mock object
id mockManager = [OCMockObject mockForClass:[MyManager class]];
[MyManager setSharedInstance:mockManager];
// we can reset it so that it returns the actual MyManager
[MyManager setSharedInstance:nil];

This also works with partial mocks, as in your example:

id mockMyManager = [OCMockObject partialMockForObject:[MyManager sharedInstance]];
[[mockMyManager expect] checkLectures];
[MyManager setSharedInstance:mockMyManager];

[schoolToTest createLecture];

[mockMyManager verify];
[mockMyManager stopMocking];
// reset it so that it returns the actual MyManager
[MyManager setSharedInstance:nil];

Here's a full breakdown of the approach.

like image 174
Christopher Pickslay Avatar answered Oct 10 '22 04:10

Christopher Pickslay


The answer is no, because you use dispatch_once(&onceToken, ^{ so even if you added another method which could reset the variable to nil you'd never be able to initialise it again.

So you already have one solution and the best solution is to not access the singleton directly (use dependency injection instead).

like image 45
Wain Avatar answered Oct 10 '22 03:10

Wain