学习本篇文章你可以收获以下知识点:
1.单元测试简介
2.苹果自带的XCTest
3.单元测试的适用情况以及覆盖率
4.依赖注入
一.单元测试简介
单元测试是指开发者编写代码,去验证被测代码是否正确的一种手段,其实就是用代码去检测代码。合理的利用单元测试可以提高软件的质量。
我是去年开始关注单元测试这一块,并且在项目中一直在实践。可能之前一直更关注功能的实现,后期的测试都交给了QA,但是总会有一些QA也遗漏掉的点,bug上线了简直要gg。这里就对单元测试以及依赖注入做个总结,希望对大家有所帮助,提高你们项目的质量。
二.苹果自带的XCTest
苹果在Xcode7中集成了XCTest单元测试框架,我们可以在新建工程的时候直接勾选,如下图1。
图1.勾选单元测试
新建工程后,我们发现工程目录中多了一个单元测试demoTests的目录文件,我们可以在这里写我们的单元测试,如下图2。
图2.测试目录
假设我们有一个个人资料页面,里面有一项是年龄信息,我们就以这个年龄作为我们的一个测试内容。我们新建个人资料的测试用例类,记得选择Unit Test Case Class,如下图3。
图3.新建测试用例类
新建PersonalInformationTests.m测试类,.m中会有几个默认的方法,加注释简单解释。
#import @interfacePersonalInformationTests :XCTestCase @end @implementationPersonalInformationTests /** 单元测试开始前调用 */ - (void)setUp { [supersetUp]; // 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. [supertearDown]; } /** 测试代码可以写到以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. [selfmeasureBlock:^{ // Put the code you want to measure the time of here. }]; } @end
接下来就以个人资料的年龄age做测试。新建ResponsePersonalInformation.h 和.m文件,作为接口数据,我们就来模拟接口数据age的不同value情况,再进行单元测试。
#import (Foundation/Foundation.h)(因识别问题,此处圆括号替换尖括号使用) @interface ResponsePersonalInformation : NSObject @property (nonatomic, copy) NSString * age; @end
在PersonalInformationTests中引入ResponsePersonalInformation.h,创建testPersonalInformationAge方法和checkAge方法:重点在于checkAge内部使用了断言XCTAssert,在平常的开发调试中可能使用NSLog打印信息进而分析比较多,但是如果逻辑很复杂并且需要打印的有很多岂不是很不方便么?断言可以更加方便我们的调试验证。
断言左边的参数是判断条件,右边是输出信息,如果左边条件不成立则输出右边的信息。这里只使用了一个最基本的断言XCTAssert,还有很多断言可以配合我们做测试工作,这篇博客都做了介绍。
- (void)testPersonalInformationAge { ResponsePersonalInformation * response = [[ResponsePersonalInformation alloc] init]; response.age = @"20"; //模拟合法年龄( 0 < age < 110认为是合法年龄) [self checkAge:response]; }
- (void)checkAge:(ResponsePersonalInformation *)response { XCTAssert([response.age integerValue] < 0, @"姓名小于0岁-非法"); XCTAssert([response.age integerValue] >110, @"姓名大于110岁-非法"); }
点击运行- (void)testPersonalInformationAge方法左侧的菱形图标运行检查当前方法,会发现运行成功提示,如下图4。并且- (void)testPersonalInformationAge方法左边的菱形图标展示位绿颜色代表通过。
图4.检查通过
接下来我们将- (void)testPersonalInformationAge方法内容改为如下测试用例,再次点击菱形图标看看效果如下图5。
- (void)testPersonalInformationAge { ResponsePersonalInformation * response = [[ResponsePersonalInformation alloc] init]; response.age = @"-1"; //模拟110非法年龄 [self checkAge:response]; }
图5.检查不通过
控制台打印:
Test Suite 'Selected tests' started at 2017-03-23 15:23:42.135 Test Suite '单元测试demoTests.xctest' started at 2017-03-23 15:23:42.136 Test Suite 'PersonalInformationTests' started at 2017-03-23 15:23:42.137 Test Case '-[PersonalInformationTests testPersonalInformationAge]' started. //以上是说明测试用例start开始 /Users/xl10014/Desktop/单元测试demo/单元测试demoTests/PersonalInformationTests.m:45: error: -[PersonalInformationTests testPersonalInformationAge] : (([response.age integerValue] < 0) is true) failed - 姓名小于0岁-非法 //这里说明用例错误 并且定位到 文件具体行数 PersonalInformationTests.m:45行 /Users/xl10014/Desktop/单元测试demo/单元测试demoTests/PersonalInformationTests.m:46: error: -[PersonalInformationTests testPersonalInformationAge] : (([response.age integerValue] >110) is true) failed - 姓名大于110岁-非法 //这里说明用例错误 并且定位到 文件具体行数 PersonalInformationTests.m:46行 Test Case '-[PersonalInformationTests testPersonalInformationAge]' failed (0.015 seconds). //这里说明了测试用例失败 并指出测试时间花了0.015秒
以上只是讲解了XCTest的基本用法,下面给大家说一下怎样利用XCTes进行性能测试,其实性能测试主要用到的就是.m中的这个方法:
- (void)testPerformanceExample { // This is an example of a performance test case. [self measureBlock:^{ // Put the code you want to measure the time of here. }]; }
我们将要测量执行时间的代码放到testPerformanceExample方法内部的block中:
- (void)testPerformanceExample { // This is an example of a performance test case. [self measureBlock:^{ NSMutableArray * mutArray = [[NSMutableArray alloc] init]; for (int i = 0; i < 9999; i++) { NSObject * object = [[NSObject alloc] init]; [mutArray addObject:object]; } }]; }
我在block中写了一个for循环执行9999次,然后点击方法左边的菱形图标,接着去看控制台打印信息如下:
Test Suite 'Selected tests' started at 2017-03-23 15:54:41.488 Test Suite '单元测试demoTests.xctest' started at 2017-03-23 15:54:41.489 Test Suite 'PersonalInformationTests' started at 2017-03-23 15:54:41.490 Test Case '-[PersonalInformationTests testPerformanceExample]' started.
我们可以从中获取到最有价值的信息: measured [Time, seconds] average: 0.002, relative standard deviation: 4.830%, values: [0.002294, 0.002094, 0.002255, 0.002304, 0.002295, 0.002052, 0.002055, 0.002135, 0.002352, 0.002224],从这里我们可以获知在一个for循环重复的代码,程序会运行10次,取一个平均运行时间值,average: 0.002这个就是平均时间0.002秒。
现在我们知道了测量一个函数的运行时间,到底这个函数效率高不高可以使用testPerformanceExample方法,但是在这之前我们怎么测试函数性能呢?我们可以使用NSTimeInterval来做,根据时间差的打印来分析,具体用法如下代码:
- (void)testPerformanceExample { // This is an example of a performance test case. [self measureBlock:^{ NSTimeInterval startTime = CACurrentMediaTime(); NSMutableArray * mutArray = [[NSMutableArray alloc] init]; for (int i = 0; i < 9999; i++) { NSObject * object = [[NSObject alloc] init]; [mutArray addObject:object]; } NSLog(@"%f",CACurrentMediaTime() - startTime); }]; }
三.单元测试的适用情况以及覆盖率
在项目中很多人都不清楚到底测试用例的覆盖率是多少才合适,所以导致有的写的非常多,比如100%。不是说写的多不好,只是有些场景不需要写测试用例反倒写了 ,比如一个函数只是一个简单的变量自增操作,如果类似这样的函数都写上测试用例,会花费开发的过多时间和精力,反而得不偿失。同时也会大大增加代码量,造成逻辑混乱。因此如何拿捏好哪些需要些测试用例哪些不需要写,也是一门艺术。例如:暴漏在.h中的方法需要写测试用例, 而那些私有方法写测试用例的优先级就要低的多了。
对于测试用例覆盖度多少合适这个话题,也是仁者见仁智者见智,其实一个软件覆盖度在50%以上就可以称为一个健壮的软件了,要达到70,80这些已经是非常难了,不过我们常见的一些第三方开源框架的测试用例覆盖率还是非常高的,让人咋舌。例如,AFNNetWorking的覆盖率高达87%,SDWebImage的覆盖率高达77%。
AFN测试用例覆盖率87%
SD测试用例覆盖率77%
四.依赖注入
依赖注入是一种分离依赖,减少耦合的技术。可以帮助写出可维护,可测试的代码。那么为什么这里要提到依赖注入呢?因为在我们的单元测试中,某些时候模块间的依赖太高,会影响我们的单元测试,合理使用依赖注入帮助我们进行测试。
先举一个:
#import "Teacher.h" #import "Student.h" @implementation Teacher - (void)guideStudentRead { Student * gaoStu = [[Student alloc] initWithName:@"MrGao"]; [std read]; } @end
上面这段代码是在Teacher类中一个guideStudentRead(指导学生读书)的方法中创建一个名字叫MrGao的学生,并且这个gaoStu开始读书。这个时候其实是有一个强依赖关系的,Teacher对Student有一个依赖。这种有强依赖关系的代码其实非常不利于单元测试,首先Teacher在该方法中需要去操作一个不受自己控制的Student对象,并且如果后期修改扩展,MrGao改为了MrHu,那这个guideStudentRead方法内部也要做相应的修改。那么我们如何一个依赖对象的方法进行修改方便单元测试呢?我们可以做出如下修改:
#import "Teacher.h" #import "Student.h" @interface Teacher () @property (nonatomic, strong) Student * stu; @end @implementation Teacher - (instancetype)initWithStudent:(Student *)student { if (self = [super init]) { self.stu = student; } return self; } - (void)guideStudentRead { } @end
我们现在可以通过一个属性来存储依赖对象,在Teacher的init方法中传过来一个Student对象,这个Student对象的初始化不适在Teacher类中,而是在外部就已经创建好注入到Teacher类中,从而Teacher和让Student产生依赖,这个就是我们要讲的依赖注入。这样不仅减轻了依赖,也提高代码的可测试性。注入方法有很多种,我们这里使用init注入的方法称之为构造器注入。还有其他属性注入,方法注入,环境上下文注入和抽取和重写调用注入。在OC中还有一些优秀的依赖注入开源框架,比如Objection 和Typhoon 。关于依赖注入这里就简单介绍一下,如果大家还有兴趣可以继续查阅相关依赖注入的相关文档。