iOS单元测试

"敏捷开发前提"

Posted by wanglilong on June 8, 2018

iOS单元测试

一、为什么需要单元测试

1.对于已稳定的功能,单元测试可以测试是否引入新的错误。

2.对于要重构的代码,单元测试是重构能否顺利进行的前提。

二、苹果自带的XCTest

1.XCTest中的测试类都是继承自XCTestCase

2.XCTest中所有的测试用例的命名都是以test开头的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#import <XCTest/XCTest.h>

@interface StudentModeTests : XCTestCase

@end

@implementation StudentModeTests

/** 单元测试开始前调用 */
- (void)setUp {
    [super setUp];
    // Put setup code here. This method is called before the invocation of each test method in the class.
}
/** 单元测试结束前调用 */
- (void)tearDown {
    // Put teardown code here. This method is called after the invocation of each test method in the class.
    [super tearDown];
}

/** 测试代码可以写到以test开头的方法中 并且test开头的方法左边会生成一个菱形图标,点击即可运行检测当前test方法内的代码 **/
- (void)testExample {
    // This is an example of a functional test case.
    // Use XCTAssert and related functions to verify your tests produce the correct results.
}

/** 测试性能 */
- (void)testPerformanceExample {
    // This is an example of a performance test case.
    [self measureBlock:^{
        // Put the code you want to measure the time of here.
    }];
}
@end

三、断言

如何判断一个测试用例成功或者失败呢?XCTest使用断言来实现。 最基本的断言,表示如果expression满足,则测试通过,否则对应format的错误。

1
XCTAssert(expression, format...)

四、性能测试

所谓性能测试,主要就是评估一段代码的运行时间

格式如下

1
2
3
4
5
6
- (void)testPerformanceExample {
    // This is an example of a performance test case.
    [self measureBlock:^{
        // Put the code you want to measure the time of here.
    }];
}

性能测试的时候,如何判一个性能测试case是成功还是失败呢?

需要有个基准,第一次运行后点击灰色点进行设置

五、异步测试

异步测试的逻辑如下,首先定义一个或者多个XCTestExpectation,表示异步测试想要的结果。然后设置timeout,表示异步测试最多可以执行的时间。最后,在异步的代码完成的最后,调用fullfill来通知异步测试满足条件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)testAsyncFunction{
    XCTestExpectation * expectation = [self expectationWithDescription:@"Just a demo expectation,should pass"];//异步测试开始
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        NSLog(@"Async test");
        XCTAssert(YES,"should pass");
        [expectation fulfill];//异步测试结束
    });
    [self waitForExpectationsWithTimeout:10 handler:^(NSError *error) {
        //Do something when time out
    }];
}

六、第三方框架

框架OCMock

1.什么是mock测试

对于一些不容易构造或不容易获取的对象,此时你可以创建一个虚拟的对象(mock object)来完成测试。

例如你可能要尝试100次才会返回一个NSError,通过mock object你可以自行创建一个NSError对象,测试在出错情况下程序的处理是否符合你的预期。

例如你要连接服务器但是服务器在实验室,你在外工作的时候就无法测试了,这个时候你可以创建一个虚拟的服务器,并返回一些你指定的数据,从而绕过服务器。

例如假设你要访问一个数据库,但是访问过程的开销巨大,这时你可以虚拟一个数据库,并且返回一些自行定制的数据,从而绕过了数据库的访问。

mock的思想很简单:没有条件?我们就自行创造条件。

2.OCMock介绍

OCMock是一个用于为iOSMac OS X项目配置Mock测试的开源项目。

其实现思想就是根据要mock的对象的class来创建一个对应的对象,并且设置好该对象的属性和调用预定方法后的动作(例如返回一个值,调用代码块,发送消息等等),然后将其记录到一个数组中,接下来开发者主动调用该方法,最后做一个verify(验证),从而判断该方法是否被调用,或者调用过程中是否抛出异常等。

其实就是可以把它当做我们伪造的一个对象,我们给它一些预设的值之类的,然后就可以进行对应的验证了。

应用场景可以是

3.OCMock例子

(1)简单的例子,为类的方法设置固定返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//最简单的一个使用OCMock的例子
- (void)testPersonNameEqual{
    
    Person *person = [[Person alloc] init];
    
    //创建一个mock对象
    id mockClass = OCMClassMock([Person class]);
    
    //可以给这个mock对象的方法设置预设的参数和返回值
    OCMStub([mockClass getPersonName]).andReturn(@"齐滇大圣");
    
    //用这个预设的值和实际的值进行比较是否相等
    XCTAssertEqualObjects([mockClass getPersonName], [person getPersonName], @"值相等");
}

(2)模拟网络请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
- (void)testDisplaysTweetsRetrievedFromConnection{
    
    TwitterViewController *controller = [[TwitterViewController alloc] init];
    
    //模拟出来一个网络连接请求数据的类
    id mockConnection = OCMClassMock([TwitterConnection class]);
    controller.connection = mockConnection;
    
    //模拟fetchTweets方法返回预设值
    Tweet *testTweet = [[Tweet alloc] init];
    testTweet.userName = @"齐滇大圣";
    Tweet *testTweet2 = [[Tweet alloc] init];
    testTweet2.userName = @"美猴王";
    NSArray *tweetArray = @[testTweet,testTweet2];
    OCMStub([mockConnection fetchTweets]).andReturn(tweetArray);
    
    //模拟出来一个view类
    id mockView = OCMClassMock([TweetView class]);
    controller.tweetView = mockView;
    
    //这里执行updateTweetView之后,[mockView addTweet:]加入了testTweet和testTweet2
    [controller updateTweetView];
    
    //---------验证使用对应参数的方法是否被调用-----------
    
    //成功
    OCMVerify([mockView addTweet:testTweet]);
    OCMVerify([mockView addTweet:testTweet2]);
    OCMVerify([mockView addTweet:[OCMArg any]]);   
    //[OCMArg any]匹配所有的参数值,既testTweet和testTweet2
    
    //失败,因为执行[controller updateTweetView];的时候,mockView没有添加testTweet3,所以验证不通过
    Tweet *testTweet3 = [[Tweet alloc] init];
    testTweet3.userName = @"斗战胜佛";
    OCMVerify([mockView addTweet:testTweet3]);
}

(3)条件限定

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)testStrictMock3{
    
    id classMock = OCMClassMock([TweetView class]);
    //这个classMock需要执行addTweet方法且参数不为nil。  不然的话会抛出异常
    
    OCMExpect([classMock addTweet:[OCMArg isNotNil]]);

    /* 如果不执行以下代码的话会抛出异常 */
//    Tweet *testTweet = [[Tweet alloc] init];
//    testTweet.userName = @"齐滇大圣";
//    [classMock addTweet:testTweet];
    OCMVerifyAll(classMock);
}

七、代码覆盖率

一个软件覆盖度在50%以上就可以称为一个健壮的软件了,要达到70,80这些已经是非常难了,不过我们常见的一些第三方开源框架的测试用例覆盖率还是非常高的,让人咋舌。例如,AFNNetWorking的覆盖率高达87%,SDWebImage的覆盖率高达77%。

选择Target,然后选择Test模块,然后勾选Gather coverage data

然后,在report模块中,就能看到每一个.m文件的代码覆盖情况了。

八、参考博客

iOS 单元测试之XCTest详解

iOS单元测试初探以及OCMock使用入门

iOS中的测试:OCMock