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

时间:2016-05-31 15:20:35

标签: ios objective-c unit-testing singleton ocmock

我正在使用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方法之外。但是我想问一下,有没有办法将实例设置为nil而不将其移到+(id)sharedInstance方法之外? (像java反射这样的解决方案吗?)

6 个答案:

答案 0 :(得分:3)

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

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

答案 1 :(得分:3)

使用本地静态变量无法实现所需。块范围的静态只在其词汇上下文中可见。

我们这样做是通过使单例实例成为作用于类实现的静态变量并添加一个mutator来覆盖它。通常,mutator仅通过测试调用。

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

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

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.

答案 2 :(得分:2)

这是一种解决问题的简单方法。 你的班级有一个单身人士。您可以添加一个销毁此类实例的方法。因此,当您再次调用shareManager方法时,它将创建一个新实例。 如:

static MyManager *sharedMyManager = nil;

+ (void)destroy
{
   sharedMyManager = nil;
}

答案 3 :(得分:1)

正如其他人所说,你应该做的是重构代码以使用依赖注入。这意味着如果School类需要MyManager实例来运行,那么它应该具有initWithManager:(MyManager *)manager方法,该方法应该是指定的初始化程序。或者,如果仅在此特定方法中需要MyManager,则它应该是方法参数,例如createLectureWithManager:(MyManager *)manager

然后在您的测试中,您可以执行School *schoolToTest = [[School alloc] initWithManager:[[MyManager alloc] init]],每个测试都会有一个新的MyManager实例。您可以完全删除单例模式,删除sharedInstance上的MyManager方法,并且您的应用程序逻辑将负责确保只传递一个实例。

但有时候,你必须处理不能重构的遗留代码。在这些情况下,您需要存根类方法。也就是说,您需要将-[MyManager sharedInstance]的实现替换为返回[[MyManager alloc] init]的实现。这可以使用运行时来调用类方法,这相当于您正在寻找的Java反射。有关如何使用运行时的示例,请参阅this

您也可以使用OCMock来实现它,后者在幕后使用运行时,就像Java中的模拟框架基于反射API一样:

MyManager *testManager = [[MyManager alloc] init];
id mock = [[OCMockObject mockForClass:[MyManager class]];
[[[mock stub] andReturn:testManager] sharedInstance];

答案 4 :(得分:0)

如果您不想重构代码以便更轻松地进行单元测试,那么还有另一种解决方案(不完美但有效):

  • 创建MyManager类型
  • 的本地属性
  • setUp中实例化上面的属性,并使用您的本地方法调整sharedInstance方法(例如swizzle_sharedInstance
  • swizzle_sharedInstance内返回本地属性
  • tearDown调回原始的sharedInstance并取消本地属性

答案 5 :(得分:0)

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

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

现在School实现将使用myManagerMock对象,您可以将此对象存根以在测试用例下返回您想要的任何内容。例如:

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

重要的是,在测试之后,您将通过调用(在测试方法结束时或在-tearDown中)来执行模拟对象的清理:

[myManagerMock stopMocking];