我正在使用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反射这样的解决方案吗?)
答案 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];
答案 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];