这篇文章介绍的是如何在swift项目中使用OCMock框架
1、Mock介绍
OCMock是一个用于为iOS或Mac OS X项目配置Mock测试的开源项目。
其实现思想就是根据要mock的对象的class来创建一个对应的对象,并且设置好该对象的属性和调用预定方法后的动作(例如返回一个值,调用代码块,发送消息等等),然后将其记录到一个数组中,接下来开发者主动调用该方法,最后做一个verify(验证),从而判断该方法是否被调用,或者调用过程中是否抛出异常等。
其实就是可以把它当做我们伪造的一个对象,我们给它一些预设的值之类的,然后就可以进行对应的验证了。
使用mock的原因:可以模拟返回数据来验证方法是否调用。
2、OCMock的使用
1)配置OCMock
OCMock的配置参见:http://ocmock.org/ios/
(1)在github下载OCMock,打开其中的/Examples/SwiftExamples,拷贝里面的usr文件夹到你所在的项目中
(2)在targets中选中测试target-->Build Phases -->Link Binary With Libraries中导入usr->lib->libOCMock.a文件
(3)设置Build Settings-->搜索 other_ld,找到Other Linker Flags -->添加 -ObjC
(4)设置Build Settings-->搜索 header search,找到Header Search Paths -->添加 $(PROJECT_DIR)/usr/include
(5)创建一个VC类和Connection类,用于mock测试
TwitterViewController.swift
import Foundation
@objcMembers
class TwitterViewController: NSObject {
var connection: Connection
var data: String
class func newController() -> TwitterViewController {
return TwitterViewController()
}
override init() {
self.connection = TwitterConnection()
self.data = ""
}
func redisplay() {
data = connection.fetchData()
}
}
TwitterConnection.swift
import Foundation
//网络连接类
@objc
protocol Connection {
func fetchData()->String
}
@objcMembers
class TwitterConnection: NSObject, Connection{
func fetchData() -> String {
return "real data returned from other system"
}
}
(6)由于是在swift项目中使用OCMock,其许多语法在swift中无法实现,所以测试文件使用OC语言书写
在文件中导入头文件
#import <OCMock/OCMock.h>
在oc文件中使用swift文件,需要导入头文件
#import "OCMockTests-Swift.h"
测试一个简单的模型类
创建一个person模型类
import UIKit
@objcMembers
class Person: NSObject {
func getPersonName()->String{
return "小李"
}
}
在测试文件中创建测试方法
- (void)testPersonNameEqual{
Person *person = [[Person alloc] init];
//创建一个mock对象
id mockClass = OCMClassMock([Person class]);
//可以给这个mock对象的方法设置预设的参数和返回值
OCMStub([mockClass getPersonName]).andReturn(@"小李");
//用这个预设的值和实际的值进行比较
XCTAssertEqualObjects([mockClass getPersonName], [person getPersonName],@"值相等");
}
mock类
例子1:使用OCMClassMock(类mock)来mock一个类对象
- (void)testMockingAnObject{
//模拟出来一个网络请求链接的数据类
id mockConnection = OCMClassMock([TwitterConnection class]);
//模拟fetchdata方法返回预设置
OCMStub([mockConnection fetchData]).andReturn(@"stubbed!");
TwitterViewController *controller = [TwitterViewController newController];
controller.connection = mockConnection;
//这里执行redisplay之后,返回 stubbed
[controller redisplay];
//-------验证使用对应参数的方法是否被调用------
//成功
OCMVerify([mockConnection fetchData]);
XCTAssertEqualObjects(@"stubbed!", controller.data, @"Excpected stubbed data in controller.");
//失败
// XCTAssertEqualObjects(@"real data returned from other system", controller.data, @"unExcpected stubbed data in controller.");
}
执行测试方法成功
主要运用于的场景:
当我们为vc来写一个测试类的时候我们要考虑它有哪些相对应的依赖,也就是TwitterConnection和data。在这个例子里我们需要实例化一个真正的TwitterConnection对象来请求真实数据然后使用它。这样的话会有几个问题:
使用真实的connection会使测试变慢,因为它还要去请求网络。
我们永远不知道每一次返回的数据是什么。
很难测试错误的返回,因为一般不会返回错误。
解决的方法就是伪造一个假的connection,既一个stub。
例子2:使用OCMPartialMock(部分mock)将新建的connection作为mock对象
- (void)testPartiallyMockingAnObject
{
//新建一个connection用来mock
TwitterConnection * testConnection = [TwitterConnection new];
id mockConnection = OCMPartialMock(testConnection);
OCMStub([mockConnection fetchData]).andReturn(@"stubbed!");
TwitterViewController *controller = [TwitterViewController newController];
[controller redisplay];
//-------验证使用对应参数的方法是否被调用------
//失败
OCMVerify([mockConnection fetchData]);
XCTAssertEqualObjects(@"stubbed!", controller.data, @"Excpected stubbed data in controller.");
}
运行测试方法,执行失败
原因是:
使用OCMPartialMock这样创建的对象在调用方法时:
如果方法被stub,调用stub后的方法.
如果方法没有被stub,调用原来的对象的方法.
partialMock 对象在调用方法后,可以用于稍后的验证此方法的调用情况(被调用,调用结果)
例子3:使用OCMPartialMock(部分mock)将使用的是controller的connection作为mock对象
- (void)testPartiallyMockingAnObject2
{
TwitterViewController *controller = [TwitterViewController newController];
//从controller中获取mock对象
id mockConnection = OCMPartialMock((NSObject *)controller.connection);
OCMStub([mockConnection fetchData]).andReturn(@"stubbed!");
[controller redisplay];
//-------验证使用对应参数的方法是否被调用------
//成功
OCMVerify([mockConnection fetchData]);
XCTAssertEqualObjects(@"stubbed!", controller.data, @"Excpected stubbed data in controller.");
}
运行测试方法,执行成功
基本语法:
关键字 | 说明 |
Mock | 创建一个模拟对象,我们可以验证,修改它的行为 |
Stub | Mock对象的函数返回特定的值 |
Partial Mock | 重写Mock对象的方法 |
3、OCMock用法总结
1)创建mock对象
用途 | 创建方式 |
类mock | 默认的mock方式是nice(方法调用的时候返回nil或者是返回正确的方法) id classMock = OCMClassMock([SomeClass class]); |
严格的类mock | 严格的模式下,mock的对象在调用没有被stub(置换)的方法的时候,会抛出异常. id classMock = OCMStrictClassMock([SomeClass class]); |
协议mock | 默认的mock方式是nice(方法调用的时候返回nil或者是返回正确的方法) id protocolMock = OCMProtocolMock(@protocol(SomeProtocol)); |
严格的协议mock | 严格的模式下,mock的对象在调用没有被stub(置换)的方法的时候,会抛出异常. id protocolMock = OCMStrictProtocolMock(@protocol(SomeProtocol)); |
部分mock | 这样创建的对象在调用方法时: 如果方法被stub,调用stub后的方法. 如果方法没有被stub,调用原来的对象的方法. partialMock 对象在调用方法后,可以用于稍后的验证此方法的调用情况(被调用,调用结果) id partialMock = OCMPartialMock(anObject) |
观察者mock | 这样创建的对象可以用于观察/通知. id observerMock = OCMObserverMock(); |
2)置换方法
置换方法类型 | 说明 |
置换方法(待置换的方法返回objects) | OCMStub([mock someMethod]).andReturn(anObject); 在mock对象上调用某个方法的时候,这个方法一定返回一个anObject.(也就是说强制替换了某个方法的返回值为anObject) |
置换方法(待置换的方法返回values) | OCMStub([mock aMethodReturningABoolean]).andReturn(YES); 在mock对象上调用某个方法的时候,这个方法一定返回values. 注意这里的原始值类型一定要和原来的方法的返回值一致. |
委托到另一个方法(置换委托方法到另外一个方法) | OCMStub([mock someMethod]).andCall(anotherObject, @selector(aDifferentMethod)); 置换mock 对象的someMethod ==> anotherObject 的aDifferentMethod. 这样,当mock对象调用someMethod方法的时候,实际上的操作就是anotherObject 调用了aDifferentMethod方法. |
置换一个blcok方法. | OCMStub([mock someMethod]).andDo(^(NSInvocation invocation) { / block that handles the method invocation */ }); 在mock对象调用someMethod的时候,andDo后面的block会调用.block可以从NSInvocation中得到一些参数,然后使用这个NSInvocation对象来构造返回值等等. |
置换方法的参数 | OCMStub([mock someMethodWithReferenceArgument:[OCMArg setTo:anObject]]);
OCMStub([mock someMethodWithReferenceArgument:[OCMArg setToValue:OCMOCK_VALUE((int){aValue})]]); mock对象在调用某个带参数的方法的时候,这个方法的参数可以被置换. setTo用来设置对象参数,setToValue用来设置原始值类型的参数. |
调用某个方法就抛出异常 | OCMStub([mock someMethod]).andThrow(anException); 当mock对象调用someMethod的时候,就会抛出异常 |
调用某个方法就发送通知 | OCMStub([mock someMethod]).andPost(aNotification); 当mock对象调用someMethod的时候,就会发送通知. |
链式调用 | OCMStub([mock someMethod]).andPost(aNotification).andReturn(aValue); 所有的actions(比如andReturn,andPost)可以链式调用.上面的例子中,mock对象调用someMethod方法后,发送通知,返回aValue |
转发的原来的对象/类 | OCMStub([mock someMethod]).andForwardToRealObject(); 使用部分mock的时候,使用类方法的可以转发到原来的对象/原来的类. 这个功能在链式调用或者是使用expectation的时候很有用. |
什么也不做 | OCMStub([mock someMethod]).andDo(nil); 可以给andDo传入nil参数,而不是原来一个block作为参数. 这个功能在使用部分mock/mock类的时候很有用,可以屏蔽原来的行为. |
3)验证作用
验证时间点 | 说明 |
运行后就验证 | id mock = OCMClassMock([SomeClass class]); /* run code under test */ OCMVerify([mock someMethod]); 在mock对象调用someMethod后就开始验证.(如果这个方法没有被调用),就抛出一个错误. 在验证语句中可以使用 参数约束. |
置换后验证 | id mock = OCMClassMock([SomeClass class]);
OCMStub([mock someMethod]).andReturn(myValue); /* run code under test */ OCMVerify([mock someMethod]); 在置换某个方法(置换了返回的参数)后,然后可以验证这个方法是否被调用. |
4)类方法的mock
用途 | 说明 |
置换类方法 | id classMock = OCMClassMock([SomeClass class]);
OCMStub([classMock aClassMethod]).andReturn(@"Test string"); // result is @"Test string" NSString *result = [SomeClass aClassMethod]; 置换类方法和置换实例方法的步骤相像.但是mock对象在深层次上对原有 类做了些更改.(替换了原有的的类的meta class).这让置换调用直接作用在mock对象上,而不是原有的类. 注意: 添加到类方法上的mock对象跨越了多个测试,mock的类对象在置换后不会deallocated,需要手动来取消这个mock关系. 如果mock对象作用于同一个类, 这时的行为就不预测了. |
验证类方法的调用 | id classMock = OCMClassMock([SomeClass class]); /* run code under test */ OCMVerify([classMock aClassMethod]); 验证类方法的调用和验证实例方法的调用的使用方式一样. |
有歧义的类方法和实例方法 | id classMock = OCMClassMock([SomeClass class]);
OCMStub(ClassMethod([classMock ambiguousMethod])).andReturn(@"Test string"); // result is @"Test string" NSString *result = [SomeClass ambiguousMethod]; 置换了类方法,但是类有一个和类方法同名的实例方法,置换类方法的时候,必须使用ClassMethod() |
恢复类 | id classMock = OCMClassMock([SomeClass class]); /* do stuff */ [classMock stopMocking]; 置换类方法后,可以将类恢复到原来的状态,通过调用stopMocking来完成. 如果在结束测试前,需要恢复到原来的状态的话,这就很有用了. 在mock对象被释放的时候,stopMocking会自动调用. 当类恢复到原来的对象,类对象的meta class会变为原来的meta class.这会移除所有的方法置换. 在调用了stopMocking之后,不应该继续使用mock对象. |
5)部分mock
用途 | 说明 |
置换方法 | id partialMock = OCMPartialMock(anObject);
OCMStub([partialMock someMethod]).andReturn(@"Test string"); // result1 is @"Test string" NSString *result1 = [partialMock someMethod]; // result2 is @"Test string", too! NSString *result2 = [anObject someMethod]; 部分Mock修改了原有的mock对象的类.(实际上是继承了待mock对象,然后替换用 继承的类来代替原有的类). 这就是说: 使用真实的对象来调用,即使是使用self,也会影响 置换方法和预期的结果. |
验证方法调用 | id partialMock = OCMPartialMock(anObject); /* run code under test */ OCMVerify([partialMock someMethod]); 验证方法的调用和验证类方法,验证协议的调用类似. |
恢复对象 | id partialMock = OCMPartialMock(anObject); /* do stuff */ [partialMock stopMocking]; 真正的对象可以通过调用stopMocking方法来恢复到原来的状态. 这种情况只有在结束测试之前需要恢复到原来状态. 部分mock对象会在释放的时候,会自动调用 stopMocking方法. 当对象转变为原来的状态后,类会变为原来的类.也会移除所有的置换方法. 在调用了stopMocking之后,最好不要去使用mock对象. |
6)观察者mock
用途 | 说明 |
准备工作 | 为观察者和通知创建一个mock对象. id observerMock = OCMObserverMock(); 在通知中心注册对象 [notificatonCenter addMockObserver:aMock name:SomeNotification object:nil]; 预期会调用这个通知. [[mock expect] notificationWithName:SomeNotification object:[OCMArg any]]; |
验证 | OCMVerifyAll(observerMock); 目前观察者 mock 总是严格的mock.当一个不在预期中的通知调用的时候,就会抛出一个异常. 这就是说,单个的通知实际上不是能被验证的.所有的通知必须按照预期赖设置.他们会在通过调用OCMVerifyAll来一起验证. |
4、总结:
优点:
1)对于一些不容易构造或不容易获取的对象,此时你可以创建一个虚拟的对象(mock object)来完成测试。
2)可以模拟很难测试的错误的返回
3)单独依靠XCTest难以完成Mock或者Stub,OCMock用到Stub和Mock来模拟这些外部依赖的对象,从而控制它们
缺点:
在swift项目中使用OCMock,测试用例的编写必须使用OC语言
参考内容: