Reset singleton instance to nil after each test case
Asked Answered
D

6

10

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

Dicker answered 31/5, 2016 at 15:20 Comment(3)
you should really use dependency injection...Chefoo
@Wain, I can use dependency injection, but anyway, I want to set the singleton instance to nil so that the next test case can have a clean starting state to create the singleton instance again. That is my question mainly a bout.Dicker
Using a state-dependent singleton is not a good idea in most cases. Either dont use a Singleton pattern or your (test)code should run without resetting the Singleton (something not happening in your real application, so it should not be mandatory for you test-code)Rushing
C
5

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.

Crochet answered 9/6, 2016 at 18:34 Comment(0)
C
4

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

Chefoo answered 1/6, 2016 at 15:35 Comment(6)
but even I use dependency injection , I still need to call [MyManager sharedInstance] at some point to create the singleton instance, but how can I reset the only instance variable to nil in teardown of my test case? Could you please make an example to be more clear? thanks.Dicker
You wouldn't use a singleton instance for testing, you'd create multiple instancesChefoo
how can I create multiple instances if my class is designed as a singleton? I don't understand how can this be done? It would be nice to show some examples.Dicker
You can still alloc init a singleton, you just aren't supposed to (sometimes it's prevented but not usually)Chefoo
I don't understand what do you mean without example.Dicker
It's singleton only by calling [MyManager sharedInstance] to retrieve the shared instance. U can actually create as many as instance with... MyManager *manager = [[MyManager alloc]init]Mccartan
K
2

It is an easier way to solute your issue. Your class have a singleton. you can add a method that is destroy this class instance. So when you call shareManager method again , it will create a new instance. Such as:

static MyManager *sharedMyManager = nil;

+ (void)destroy
{
   sharedMyManager = nil;
}
Kingdon answered 7/6, 2016 at 2:49 Comment(0)
W
1

As others have stated, what you should really do is refactor your code to use dependency injection. This means that if the School class needs a MyManager instance to operate, then it should have an initWithManager:(MyManager *)manager method which should be the designated initializer. Or if the MyManager is only needed in this particular method, it should be a method parameter, e.g. createLectureWithManager:(MyManager *)manager.

Then in your tests, you could just do School *schoolToTest = [[School alloc] initWithManager:[[MyManager alloc] init]], and each test would have a new MyManager instance. You could drop the singleton pattern entirely, removing the sharedInstance method on MyManager and your application's logic would be responsible to ensure that there is only one instance that you pass around.

But sometimes, you have to work with legacy code that you can't just refactor. In these cases, you need to stub the class method. That is, you need to replace the implementation of -[MyManager sharedInstance] with an implementation that returns [[MyManager alloc] init]. This can be accomplished using the runtime to swizzle the class method, which would be the equivalent of Java reflection that you are looking for. See this for an example of how to use the runtime.

You can also do it with OCMock, which uses the runtime behind the scenes, just like mocking frameworks in Java are based on the reflection API :

MyManager *testManager = [[MyManager alloc] init];
id mock = [[OCMockObject mockForClass:[MyManager class]];
[[[mock stub] andReturn:testManager] sharedInstance];
Wisent answered 3/6, 2016 at 15:41 Comment(0)
N
0

If you don't want to refactor your code for easier unit testing then there is another solution (not perfect but works):

  • Create a local property of MyManager type
  • In setUp instantiate the property from above and swizzle the sharedInstance method with your local method (e.g. swizzle_sharedInstance)
  • Inside the swizzle_sharedInstance return the local property
  • In tearDown swizzle back to original sharedInstance and nullify the local property
Nation answered 9/6, 2016 at 17:39 Comment(0)
C
0

I suggest a little bit different approach. You can create a mock of your sharedInstance using OCMock:

id myManagerMock = OCMClassMock([MyManager class]);
OCMStub([myManagerMock sharedManager]).andReturn(myManagerMock);

Now School implementation will use myManagerMock object, and you can stub this object to return anything you want under you test case. For example:

OCMStub([myManagerMock someMethodThatReturnsBoolean]).andReturn(YES);

It's important that after your tests, you will perform cleaning of your mock object by calling (at the end of your test method or in -tearDown):

[myManagerMock stopMocking];
Collar answered 9/6, 2016 at 19:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.