0

我正在尝试检查是否使用 OCMock 调用了类方法。我从 OCMock 网站和其他关于 SO 的答案收集到新的 OCMock 版本(2.1)增加了对存根类方法的支持。

我正在尝试做同样的事情:

细节视图控制器:

+(BOOL)getBoolVal
{
return YES;
}

测试用例:

-(void) testClassMethod
{
id detailMock = [OCMockObject mockForClass:[DetailViewController class]];

[[[detailMock stub] andReturnValue:OCMOCK_VALUE((BOOL){YES})] getBoolVal:nil];
}

测试正在运行并且也成功,但即使我从getBoolVal方法中返回 NO 而不是 YES,它也会成功DetailViewController。在该方法上保留断点时,测试执行不会停止指示该方法未被调用。

那我该如何检查一个类方法呢?

4

2 回答 2

0

编辑

只是更仔细地查看了 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

那么这里发生了什么?首先有一些宏,我将在下面讨论。接下来我重写了类方法,让它调用实例方法。我们可以使用OCMockto mock 实例方法,所以通过让类方法调用实例方法,我们可以让类方法调用 mock。

虽然我们不想在我们的真实代码中使用 FakeNSURLConnection,但我们确实想在我们的测试中使用它。我们应该怎么做?我们可以在和之间调换类方法。这意味着在我们用 call调配一个调用之后。这将我们带到了我们的宏:NSURLConnectionFakeNSURLConnectionNSURLConnection connectionWithRequest:delegateFakeNSURLConnection 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 总是会被执行。

可能还有其他模拟技术可以使这更简单,尽管我发现它并没有那么糟糕,因为你只写一次就可以多次使用它。

于 2013-05-10T16:42:28.257 回答
0

也许我在这里遗漏了一些东西(我知道这有点旧),但是你的类签名是+(BOOL)getBoolVal { return YES; }并且在你的测试中,你正在调用 expectgetBoolVal:nil

那些不匹配,对吧?在这种情况下,您的班级模拟会说,“哦,这不是我期望的签名”,并尝试将其传递给下级课程,我相信。请参阅OCMock 源代码forwardInvocationForClassObject中的。

至于为什么你得到 NO(因为底层类也返回 YES,这使得这个测试有点实际意义,但这是另一个问题),我不是 100% 确定,但也许这只是一个 C 主义—— '不确定值,无效,0(假/否)"

于 2013-08-15T16:54:09.877 回答