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

10

我正在使用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;
}

我在School类中有一个方法,它使用上述的单例:

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

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

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

我知道在互联网上,有些人建议将static MyManager *sharedMyManager移到+(id)sharedInstance方法之外。但是我想问一下,是否有任何方法可以在不移动它到+(id)sharedInstance方法之外的情况下将实例设置为nil?(有没有类似于Java反射的解决方案?)


2
你真的应该使用依赖注入... - Wain
@Wain,我可以使用依赖注入,但是无论如何,我都想将单例实例设置为nil,以便下一个测试用例可以拥有一个干净的起始状态来再次创建单例实例。这就是我的主要问题所在。 - Leem.fin
在大多数情况下,使用状态相关的单例模式并不是一个好主意。要么不使用单例模式,要么你的(测试)代码应该在不重置单例的情况下运行(这在你的实际应用程序中并没有发生,因此对于你的测试代码来说不是强制性的)。 - Jonathan Cichon
6个回答

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

然后在您的单元测试中:

// 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];

这也适用于部分 mock,就像你的示例一样:
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];

这里有一份完整的方法介绍,涉及到Objective-C中的单例模式和单元测试。


4
答案是否定的,因为你使用了dispatch_once(&onceToken, ^{,所以即使你添加了另一个方法可以将变量重置为nil,你也永远无法再次初始化它。
所以你已经有了一个解决方案,最好的解决方案是不直接访问单例(改用依赖注入)。

即使我使用了依赖注入,但我仍然需要在某个时刻调用[MyManager sharedInstance]来创建单例实例,但是在测试用例的拆卸中,如何将唯一实例变量重置为nil呢?您能否提供一个示例以更清晰地说明此问题?谢谢。 - Leem.fin
您不会在测试中使用单例实例,而是会创建多个实例。 - Wain
如果我的类被设计为单例模式,我该如何创建多个实例?我不明白这是怎么做到的。能否提供一些示例呢? - Leem.fin
你仍然可以使用alloc init创建单例,但是你不应该这样做(有时会被阻止,但通常不会)。 - Wain
我不明白你的意思,没有例子我无法理解。 - Leem.fin
1
只有通过调用[MyManager sharedInstance]来检索共享实例才能实现单例。您实际上可以使用...创建尽可能多的实例。 MyManager *manager = [[MyManager alloc]init] - Codebear

2

这是一个更简单的解决问题的方法。 您的类具有单例模式。您可以添加一个方法来销毁此类实例。因此,当您再次调用shareManager方法时,它将创建一个新的实例。 例如:

static MyManager *sharedMyManager = nil;

+ (void)destroy
{
   sharedMyManager = nil;
}

1
正如其他人所说,你真正需要做的是重构你的代码以使用依赖注入。这意味着如果 School 类需要一个 MyManager 实例来操作,那么它应该有一个 initWithManager:(MyManager *)manager 方法,这应该是指定的初始化方法。或者如果 MyManager 只在这个特定的方法中需要,那么它应该是一个方法参数,例如 createLectureWithManager:(MyManager *)manager
然后在你的测试中,你可以只做 School *schoolToTest = [[School alloc] initWithManager:[[MyManager alloc] init]],每个测试都会有一个新的 MyManager 实例。你可以完全放弃单例模式,删除 MyManager 上的 sharedInstance 方法,你的应用程序逻辑将负责确保只有一个实例传递。
但有时候,你必须处理无法重构的旧代码。在这种情况下,你需要桩定类方法。也就是说,你需要用一个返回[[MyManager alloc] init]的实现来替换-[MyManager sharedInstance]的实现。可以使用运行时来交换类方法,这相当于你要寻找的Java反射的等价物。请参见this以获取如何使用运行时的示例。
你还可以使用OCMock来完成,它与Java中的模拟框架一样,使用底层的运行时。
MyManager *testManager = [[MyManager alloc] init];
id mock = [[OCMockObject mockForClass:[MyManager class]];
[[[mock stub] andReturn:testManager] sharedInstance];

0

如果您不想为更轻松的单元测试重构代码,则有另一种解决方案(不完美但有效):

  • 创建一个MyManager类型的本地属性
  • setUp中从上面实例化属性,并用您的本地方法(例如swizzle_sharedInstance)交换sharedInstance方法
  • swizzle_sharedInstance内返回本地属性
  • tearDown中切换回原始的sharedInstance并将本地属性设置为空

0

我建议采用稍微不同的方法。您可以使用OCMock创建sharedInstance的模拟:

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

现在 School 的实现将使用 myManagerMock 对象,您可以在测试用例下存根此对象以返回任何您想要的内容。例如:
OCMStub([myManagerMock someMethodThatReturnsBoolean]).andReturn(YES);

在测试完成后,重要的是通过调用(在测试方法的末尾或-tearDown中)清理您的模拟对象:

[myManagerMock stopMocking];

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接