swift单元测试(七)三方模拟框架OCMock的使用

这篇文章介绍的是如何在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语言

 

参考内容:

OC:OCMock 用法一二

iOS中的测试:OCMock

[iOS单元测试系列]-译-OCMock常见使用方式

猜你喜欢

转载自blog.csdn.net/lin1109221208/article/details/93051476