38

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

简化示例:

- (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 类别对象来完成这种事情吗?

4

5 回答 5

46

OCMock 3 更新

OCMock 对其语法进行了现代化改造,以支持类方法存根:

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

更新

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

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

请参阅OCMock 的功能

原始答案

Barry Wark 回答后的示例代码。

假类,只是存根 connectionWithRequest:delegate:

@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);
}
于 2012-12-05T00:11:32.457 回答
18

来自 Ruby 世界,我完全理解您想要完成的工作。显然,你今天比我早三个小时试图做同样的事情(时区的事情?:-)。

无论如何,我相信OCMock 不支持这种方式,因为存根类方法需要从字面上进入类并更改其方法实现,而不管何时、何地或谁调用该方法。这与 OCMock 似乎所做的相反,后者为您提供一个代理对象,您可以直接操作或以其他方式对其进行操作,以代替指定类的“真实”对象。

例如,想要存根 NSURLConnection +sendSynchronousRequest:returningResponse:error: 方法似乎是合理的。然而,在我们的代码中使用这个调用是很典型的,因此参数化它并为 NSURLConnection 类交换一个模拟对象非常尴尬。

出于这个原因,我认为您发现的“方法混合”方法虽然不性感,但正是您想要对类方法进行存根。说它非常繁琐似乎很极端——我们是否同意它“不优雅”并且可能不如 OCMock 为我们创造生活那么方便。尽管如此,这是一个非常简洁的问题解决方案。

于 2009-11-28T00:00:45.507 回答
6

这是一个不错的“要点”,其中包含类方法的 swizzle 实现:https ://gist.github.com/314009

于 2011-02-21T02:27:57.193 回答
4

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

于 2009-11-28T00:49:12.303 回答
2

链接到问题中的博文和 RefuX gist 启发了我想出他们的想法的块启用实现:https ://gist.github.com/1038034

于 2011-06-21T15:08:54.080 回答