iOS中的测试:OCMock
Mock介绍
什么是mock测试?
对于一些不容易构造或不容易获取的对象,此时你可以创建一个虚拟的对象(mock object)来完成测试。
例如你可能要尝试100次才会返回一个NSError,通过mock object你可以自行创建一个NSError对象,测试在出错情况下程序的处理是否符合你的预期。
例如你要连接服务器但是服务器在实验室,你在外工作的时候就无法测试了(小弟就试过这种情况,非常反感),这个时候你可以创建一个虚拟的服务器,并返回一些你指定的数据,从而绕过服务器。
例如假设你要访问一个数据库,但是访问过程的开销巨大,这时你可以虚拟一个数据库,并且返回一些自行定制的数据,从而绕过了数据库的访问。
mock的思想很简单:没有条件?我们就自行创造条件。
OCMock介绍
OCMock是一个用于为iOS或Mac OS X项目配置Mock测试的开源项目。
其实现思想就是根据要mock的对象的class来创建一个对应的对象,并且设置好该对象的属性和调用预定方法后的动作(例如返回一个值,调用代码块,发送消息等等),然后将其记录到一个数组中,接下来开发者主动调用该方法,最后做一个verify(验证),从而判断该方法是否被调用,或者调用过程中是否抛出异常等。
其实就是可以把它当做我们伪造的一个对象,我们给它一些预设的值之类的,然后就可以进行对应的验证了。
配置
到OCMock的官网下载dmg文件,打开后里面有个iOS library
文件夹。把iOS library
里的文件加入到你的项目里,按这篇教程来进行配置。
OCMock文档,可以到这里查看如何详细使用OCMock。
例子中使用到的方法说明
(以下数字为OCMock文档中的数字目录)
1.1. Class mocks
id classMock = OCMClassMock([SomeClass class]);
创建mock object当做类的实例。
2.1. Stubbing methods that return objects
OCMStub([mock someMethod]).andReturn(anObject);
告诉mock object当它调用someMethod的时候应该返回anObject。
3.1. Verify-after-running
id mock = OCMClassMock([SomeClass class]);
/* run code under test */
OCMVerify([mock someMethod]);
/* run code under test */写一些对应的代码,然后 OCMVerify([mock someMethod]);
验证方法是否被调用。
3.2. Stubs and verification
id mock = OCMClassMock([SomeClass class]);
OCMStub([mock someMethod]).andReturn(myValue);
/* run code under test */
OCMVerify([mock someMethod]);
4.1. The any constraint
OCMStub([mock someMethodWithAnArgument:[OCMArg any]])
OCMStub([mock someMethodWithPointerArgument:[OCMArg anyPointer]])
OCMStub([mock someMethodWithSelectorArgument:[OCMArg anySelector]])
实际例子
以下的例子Demo:DSOCMockDemo
例子1
使用了文档中的OCMock文档中以下几种方法:
1.1 Class mocks
2.1 Stubbing methods that return objects
3.1 Verify-after-running
3.2 Stubs and verification
4.1 The any constraint
这是一个来自于OCMock上的例子。我这里就做一下翻译和Demo。
为了使我们更加具体的理解使用OCMock,这里假设我们写了一个接收来自于Twitter信息的应用。
有一个TwitterViewController类,一个TwitterConnection用来调用Twitter API得到数据,和一个TweetView类用来显示tweet对象。
下面是TwitterViewController
类,有connection
和tweetView
对象。
@interface TwitterViewController : UIViewController
@property(nonatomic, strong)TwitterConnection *connection;
@property(nonatomic, strong)TweetView *tweetView;
- (void)updateTweetView;
@end
TwitterConnection是一个网络连接类,有一个fetchTweets方法用来接收请求回来的信息。返回一个Tweet对象的数组。
@interface TwitterConnection : NSObject
- (NSArray *)fetchTweets;
@end
TweetView
有一个addTweet:
方法用来添加每一个Tweet对象到页面上。
@interface TweetView : UIView
- (void)addTweet:(Tweet *)aTweet;
@end
为什么使用mocks用来测试
当我们为updateTweetView来写一个测试类的时候我们要考虑它有哪些相对应的依赖,也就是TwitterConnection和TweetView。在这个例子里我们需要实例化一个真正的TwitterConnection对象来请求真实数据然后使用它。这样的话会有几个问题:
- 使用真实的connection会使测试变慢,因为它还要去请求网络。
- 我们永远不知道每一次Twitter返回的数据是什么。
- 很难测试错误的返回,因为Twitter一般不会返回错误。
解决的方法就是伪造一个假的connection,既一个stub。
下图就是使用测试的代码:
例子2
OCMock会在mock实例上没有找到相同名字的实例方法的时候去找同名的类方法。
使用例子一种的类,给TwitterConnection
加个类方法
+ (NSArray *)fetchTweets2;
。
我们可以看到例子2和例子中classMethod和instanceMethod的stub方式一样。
例子3
使用了文档中的OCMock文档中以下几种方法:
1.3 Strict class and protocol mocks
7.1 Expect-run-verify
7.2 Strict mocks and failing fast
7.3 Stub actions and expect
当我们使用普通的mock的时候是这样的:
- (void)testStrictMock3{
id classMock = OCMClassMock([TweetView class]);
//设置期望或预设,这个classMock需要执行addTweet方法且参数不为nil。 不然的话会抛出异常
//OCMExpect([classMock addTweet:[OCMArg isNotNil]]);
//OCMStub([classMock addTweet:[OCMArg isNotNil]]);
/* 如果不执行以下代码的话会抛出异常 */
Tweet *testTweet = [[Tweet alloc] init];
testTweet.userName = @"齐滇大圣";
[classMock addTweet:testTweet];
OCMVerifyAll(classMock);
}
这表示一种友好的mock,不会在没有OCMExpect或OCMStub设置类的所有方法时抛出异常。以上代码把OCMExpect和OCMStub注释掉时不会报错。
还有一种表示严格的mock:OCMStrictClassMock
,如果把OCMExpect和OCMStub注释掉时会报错,它要求你执行类中的所有方法,所以比较适合用来测试必须实现的方法,代码如下:
- (void)testStrictMock3{
id classMock = OCMStrictClassMock([TweetView class]);
//OCMExpect([classMock addTweet:[OCMArg isNotNil]]);
//OCMStub([classMock addTweet:[OCMArg isNotNil]]);
Tweet *testTweet = [[Tweet alloc] init];
testTweet.userName = @"齐滇大圣";
[classMock addTweet:testTweet];
OCMVerifyAll(classMock);
}
参考
ocmock源码
[iOS单元测试系列]单元测试编码规范
[iOS单元测试系列]-译-OCMock常见使用方式
Introduction to mocking with OCMock