在每个测试用例之后将单例实例重置为nil

Lee*_*fin 10 singleton unit-testing objective-c ocmock ios

我正在使用OCMock 3来测试我的iOS项目.

我使用dispatch_once()创建了一个单例类MyManager:

@implementation MyManager

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

我在School课堂上有一个使用上述单例的方法:

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

现在,我想对这个方法进行单元测试,我使用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];
}
Run Code Online (Sandbox Code Playgroud)

tearDown阶段,我想将单例实例设置为nil以便以下测试用例可以从干净状态开始.

我知道在互联网上,有些人建议将方法移到static MyManager *sharedMyManager外面+(id)sharedInstance.但我想问一下,有没有办法将实例设置为nil而不将其移到外部+(id)sharedInstance方法?(像java反射这样的解决方案?)

Chr*_*lay 5

你不能用局部静态变量实现你想要的。块范围的静态变量仅在其词法上下文中可见。

我们通过使单例实例成为作用域为类实现的静态变量并添加一个修改器来覆盖它来做到这一点。通常,该修改器仅由测试调用。

@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
Run Code Online (Sandbox Code Playgroud)

然后在您的单元测试中:

// 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];
Run Code Online (Sandbox Code Playgroud)

这也适用于部分模拟,如您的示例所示:

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];
Run Code Online (Sandbox Code Playgroud)

这是该方法的完整细分。


Wai*_*ain 4

答案是否定的,因为dispatch_once(&onceToken, ^{即使您添加了另一种可以将变量重置为零的方法,您也将永远无法再次初始化它。

所以你已经有了一种解决方案,最好的解决方案是不直接访问单例(而是使用依赖注入)。