编辑
只是更仔细地查看了 OCMock 的最新版本,我认为您在解释模拟您直接测试的类方法的方式方面是正确的,那么问题仅仅是您使用错误的签名调用它吗?
下面的答案是一般情况(当被测方法调用类方法时也很有用)。
原来的
使用 OCMock 检查类方法有点棘手。您当前正在做的是创建一个名为 detailMock 的模拟对象并存根一个名为 getBoolVal 的实例方法:(顺便说一下,您的方法原型不带参数,因此您不应该将 nil 传递给它——真正挑剔,如果你想遵循 Apple 的指导方针,他们建议不要在 getter 中使用“get”这个词(除非你发送一个指针引用来设置)。编译不会失败,因为 detailMock 是一个 id 并且愿意响应任何选择器。
那么如何测试一个Class方法呢?对于一般情况,您需要进行一些调整。这是我的做法。
让我们看看我们如何伪造NSURLConnection
您也应该能够将其应用于您的班级。
首先扩展您的课程:
@interface FakeNSURLConnection : NSURLConnection
+ (id)sharedInstance;
+ (void)setSharedInstance:(id)sharedInstance;
+ (void)enableMock:(id)mock;
+ (void)disableMock;
- (NSURLConnection *)connectionWithRequest:(NSURLRequest *)request delegate:(id<NSURLConnectionDelegate>)delegate;
@end
请注意,我对测试 connectionWithRequest:delegate 很感兴趣,并且我已经扩展了该类以添加与类方法具有相同签名的公共实例方法。让我们看一下实现:
@implementation FakeNSURLConnection
SHARED_INSTANCE_IMPL(FakeNSURLConnection);
SWAP_METHODS_IMPL(NSURLConnection, FakeNSURLConnection);
DISABLE_MOCK_IMPL(FakeNSURLConnection);
ENABLE_MOCK_IMPL(FakeNSURLConnection);
+ (NSURLConnection *)connectionWithRequest:(NSURLRequest *)request delegate:(id<NSURLConnectionDelegate>)delegate {
return [FakeNSURLConnection.sharedInstance connectionWithRequest:request delegate:delegate];
}
- (NSURLConnection *)connectionWithRequest:(NSURLRequest *)request delegate:(id<NSURLConnectionDelegate>)delegate { return nil; }
@end
那么这里发生了什么?首先有一些宏,我将在下面讨论。接下来我重写了类方法,让它调用实例方法。我们可以使用OCMock
to mock 实例方法,所以通过让类方法调用实例方法,我们可以让类方法调用 mock。
虽然我们不想在我们的真实代码中使用 FakeNSURLConnection,但我们确实想在我们的测试中使用它。我们应该怎么做?我们可以在和之间调换类方法。这意味着在我们用 call调配一个调用之后。这将我们带到了我们的宏:NSURLConnection
FakeNSURLConnection
NSURLConnection connectionWithRequest:delegate
FakeNSURLConnection connectionWithRequest:delegate
#define SWAP_METHODS_IMPL(REAL, FAKE) \
+ (void)swapMethods \
{ \
Method original, mock; \
unsigned int count; \
Method *methodList = class_copyMethodList(object_getClass(REAL.class), &count); \
for (int i = 0; i < count; i++) \
{ \
original = class_getClassMethod(REAL.class, method_getName(methodList[i])); \
mock = class_getClassMethod(FAKE.class, method_getName(methodList[i])); \
method_exchangeImplementations(original, mock); \
} \
free(methodList); \
}
#define DISABLE_MOCK_IMPL(FAKE) \
+ (void)disableMock \
{ \
if (_mockEnabled) \
{ \
[FAKE swapMethods]; \
_mockEnabled = NO; \
} \
}
#define ENABLE_MOCK_IMPL(FAKE) \
static BOOL _mockEnabled = NO; \
+ (void)enableMock:(id)mockObject; \
{ \
if (!_mockEnabled) \
{ \
[FAKE setSharedInstance:mockObject]; \
[FAKE swapMethods]; \
_mockEnabled = YES; \
} \
else \
{ \
[FAKE disableMock]; \
[FAKE enableMock:mockObject]; \
} \
}
#define SHARED_INSTANCE_IMPL() \
+ (id)sharedInstance \
{ \
return _sharedInstance; \
}
#define SET_SHARED_INSTANCE_IMPL() \
+ (void)setSharedInstance:(id)sharedInstance \
{ \
_sharedInstance = sharedInstance; \
}
我会推荐这样的东西,这样你就不会意外地重新调整你的类方法。那么你将如何使用它呢?
id urlConnectionMock = [OCMockObject niceMockForClass:FakeNSURLConnection.class];
[FakeNSURLConnection enableMock:urlConnectionMock];
[_mocksToDisable addObject:FakeNSURLConnection.class];
[[[urlConnectionMock expect] andReturn:urlConnectionMock] connectionWithRequest:OCMOCK_ANY delegate:OCMOCK_ANY];
差不多就是这样——你已经混合了方法,所以你的假类将被调用,这将调用你的模拟。
啊,但最后一件事。_mocksToDisable 是一个NSMutableArray
包含我们调配的每个类的类对象。
- (void)tearDown
{
for (id mockToDisable in _mocksToDisable)
{
[mockToDisable disableMock];
}
}
我们这样做是tearDown
为了确保在测试运行后我们已经对我们的类进行了解调——不要在测试中正确地这样做,因为如果出现异常,并不是所有的测试代码都会被执行,但 tearDown 总是会被执行。
可能还有其他模拟技术可以使这更简单,尽管我发现它并没有那么糟糕,因为你只写一次就可以多次使用它。