如何在OCMock中存根一个类方法?

时间:2009-11-27 18:12:32

标签: iphone objective-c cocoa-touch ocmock

我经常在我的iPhone Objective-C单元测试中发现我想要一个类方法,例如NSUrlConnection的+ sendSynchronousRequest:returningResponse:error:method。

简化示例:

- (void)testClassMock
{
    id mock = [OCMockObject mockForClass:[NSURLConnection class]];
    [[[mock stub] andReturn:nil] sendSynchronousRequest:nil returningResponse:nil error:nil];
}

运行时,我得到:

Test Case '-[WorklistTest testClassMock]' started.
Unknown.m:0: error: -[WorklistTest testClassMock] : *** -[NSProxy doesNotRecognizeSelector:sendSynchronousRequest:returningResponse:error:] called!
Test Case '-[WorklistTest testClassMock]' failed (0.000 seconds).

我很难找到任何关于此的文档,但我认为OCMock不支持类方法。

经过大量谷歌搜索,我发现了这个提示。它有效,但非常麻烦: http://thom.org.uk/2009/05/09/mocking-class-methods-in-objective-c/

在OCMock中有没有这样做?或者有人会想到一个聪明的OCMock类别对象,可以编写它来完成这类事情吗?

5 个答案:

答案 0 :(得分:45)

更新OCMock 3

OCMock已经使其语法现代化,以支持类方法存根:

id classMock = OCMClassMock([SomeClass class]);
OCMStub(ClassMethod([classMock aMethod])).andReturn(aValue);

<强>更新

OCMock现在支持开箱即用的类方法存根。 OP的代码现在应该按照发布的方式工作。如果存在与类方法同名的实例方法,则语法为:

[[[[mock stub] classMethod] andReturn:aValue] aMethod]

请参阅OCMock's Features

原始答案

Barry Wark回答后的示例代码。

伪造的类,只是存根连接,具有连接:委托:

@interface FakeNSURLConnection : NSURLConnection
+ (id)sharedInstance;
+ (void)setSharedInstance:(id)sharedInstance;
+ (NSURLConnection *)connectionWithRequest:(NSURLRequest *)request delegate:(id<NSURLConnectionDelegate>)delegate;
- (NSURLConnection *)connectionWithRequest:(NSURLRequest *)request delegate:(id<NSURLConnectionDelegate>)delegate;
@end
@implementation FakeNSURLConnection
static id _sharedInstance;
+ (id)sharedInstance { if (!_sharedInstance) { _sharedInstance = [self init]; } return _sharedInstance; }
+ (void)setSharedInstance:(id)sharedInstance { _sharedInstance = sharedInstance; }
+ (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

切换到模拟:

{
    ...
    // Create the mock and swap it in
    id nsurlConnectionMock = [OCMockObject niceMockForClass:FakeNSURLConnection.class];
    [FakeNSURLConnection setSharedInstance:nsurlConnectionMock];
    Method urlOriginalMethod = class_getClassMethod(NSURLConnection.class, @selector(connectionWithRequest:delegate:));
    Method urlNewMethod = class_getClassMethod(FakeNSURLConnection.class, @selector(connectionWithRequest:delegate:));
    method_exchangeImplementations(urlOriginalMethod, urlNewMethod);

    [[nsurlConnectionMock expect] connectionWithRequest:OCMOCK_ANY delegate:OCMOCK_ANY];

    ...
    // Make the call which will do the connectionWithRequest:delegate call
    ...

    // Verify
    [nsurlConnectionMock verify];

    // Unmock
    method_exchangeImplementations(urlNewMethod, urlOriginalMethod);
}

答案 1 :(得分:17)

来自Ruby的世界,我完全理解你想要完成的事情。显然,你真的提前三个小时试图做同样的事情(时区的事情?: - )。

无论如何,我相信在OCMock中不支持这种方式,因为存在类方法需要逐字地进入类并更改其方法实现,无论何时何地,或谁叫这个方法。这与OCMock似乎所做的相反,它为您提供了一个代理对象,您可以直接操作该代理对象并代替指定类的“真实”对象。

例如,想要存根NSURLConnection + sendSynchronousRequest:returningResponse:error:方法似乎是合理的。但是,通常在我们的代码中使用这个调用有点埋没,因此使它参数化并在NSURLConnection类的模拟对象中交换非常尴尬。

出于这个原因,我认为你发现的“方法调配”方法虽然不性感,但正是你想要用来存根类方法的。说它非常非常麻烦似乎极端 - 我们怎么认为它“不优雅”,也许不像OCMock为我们生活那么方便。尽管如此,这是解决问题的一个非常简洁的解决方案。

答案 2 :(得分:6)

这是一个很好的'要点',为类方法提供了一个混合实现:https://gist.github.com/314009

答案 3 :(得分:4)

如果你修改你的测试方法以获取一个注入NSURLConnection类的参数,那么传递一个响应给定选择器的模拟相对容易(你可能需要创建一个虚拟类)在你的测试模块中,它将选择器作为实例方法并模拟该类)。如果没有这个注入,你将使用一个类方法,基本上使用NSURLConnection(类)作为单例,因此已经陷入使用单例对象的反模式,并且代码的可测试性受到了影响。

答案 4 :(得分:2)

链接到问题中的博客文章,RefluX的主旨激励我提出阻止他们的想法实现:https://gist.github.com/1038034