重要!
- 对于即将新增的功能函数,一定要先写UT再写实现代码。因为这样可以验收代码设计的角度避免一些思路问题。这一点很重要,也很好理解,资料很多就不赘述了。
- 对于已有的代码,后面补UT Case的过程其实相当于code review,也是一个代码优化的过程。虽然可能会觉得枯燥且无用功,但是写完或者在写的过程中会有很多收获的,就如这篇文章记录了我的收获一样。
一个很好用的网站,不用谢
问题和技巧
- 用断言来表达值得的范围
//如网络状态的返回值是枚举,枚举的值有{-1,1,2,3,4},所以返回值必须小于4且不等于0
NSInteger netClassNotContainWifi = [Reachability getMobileNetworkClass];
XCTAssertNotEqual(0, netClassNotContainWifi,@"返回值必须小于4且不等于0");
XCTAssertLessThanOrEqual(netClassNotContainWifi, 4, @"返回值必须小于4且不等于0");
- 可以用OCMock 来模拟一些测试环境或者比较难给出的输入
//此处为mock 的数据,直接搜OCMock
NSInteger fd = [Reachability create3GOr4GFD];
XCTAssertFalse(fd != -1, @"创建");//simulator use WiFi ,so the case must be failed
- 对于一些基本的工具类,不涉及逻辑的其实可以不用写UT
如下面的代码是对获取时间类的方法。里面的实现不怎么涉及到逻辑,只是简单的函数封装。
原则上是要写的(如果时间允许还是要写的,谁知道谁会在哪个时间不小心改了啥呢,毕竟UT是用来干这个事情的),我写了,写完觉得没必要,写这个帖子的时候又觉得很有必要,哈哈哈。
//被测试代码如下两个方法
+ (long)getUnixTime {
return [[self class] getUnixTime:[NSDate date]];
}
+ (long)getUnixTime:(NSDate *)date {
long time;
NSDate *fromdate= date;
time=(long)[fromdate timeIntervalSince1970];
return time;
}
#import
#import "DLNSDateUtil.h"
@interface DLNSDateUtilTests : XCTestCase
@end
@implementation DLNSDateUtilTests
- (void)testExample {
long unixTime = [DLNSDateUtil getUnixTime];
sleep(1);
long unixTimeNow = [DLNSDateUtil getUnixTime:[NSDate date]];
XCTAssertTrue((unixTimeNow > unixTime), @"感觉这个类不需要测试");
}
- 遇到相同功能但是函数名不同的已有代码,或者命名不合理的,果断修改
比如上面DLNSDateUtil
类中,getUnixTime
一般默认返回当前时间,但是原有代码还有一个方法是getUnixTimeNow
如下
+ (long)getUnixTimeNow {
return [[self class] getUnixTime];
}
真不知道原来的人怎么想的,果断删除getUnixTimeNow
- 遇到已有代码中(后写UT的场景),有些功能函数整个项目中压根就没有使用到的方法,是否需要写对应的UT呢
举个:比如一个文件读写类,对外暴露了增删改查四个功能,项目中此时只用到其中的增删改三个,查询方法暂时没有用到,那么这个时候有2种处理方式:
- 直接删除这个没有用到的方法
- 继续写UT
这里我的思路是这样的:对于SOLID里的S原则来说,文件读写类必须有增删改查四个功能,即使此时用不到,如果少了查询功能,那么你的功能函数是不完整的。一个函数干一个事,一个功能必须四个干事的函数。你现在用不到,可能你的同时负责的功能能用到呢,或者你以后能用到呢。所以这里是要写UT的。
- 遇到压根就不用的类怎么办(可能以前用,现在不用了。或者从来就没有用过)
其实这个就不是UT的范畴了,这里和同事商量了下:在写UT初期,先跳过这样的类。加个TODO标记下。
我一共尝试了两种方式
- 从业务角度出发:
从你业务最开始的逻辑部分开始,一层层剥离开,然后去到那个单一功能类,比如文件管理类,然后针对该类的函数写UT Case
2.从代码结构出发,一般项目都会有Utils Handler Http这样的底层代码文件夹。直接在UT文件夹下建立类似的文件目录,方便管理测试代码。而不是UT demo 工程那么随意放置。
第一种不可取,因为业务逻辑没个定型,刚开始还好,然后看着看着就不知道去哪里了,迷失了自我。第二种则思路比较清晰,也能客观的看这个类的封装及功能设计是否合理。
- iOS对已有的项目写UT有说到UT要写什么,怎么写,下面这一段就是例子
对于一个获取IP 的类有下面三个函数,很常规的吧
+ (NSString *)getWifiIPAddress;
+ (NSString *)getCellularIPAddress;
+ (NSMutableArray *)resolveDomainName:(NSString *)domainName;
@implementation NetworkAddressTests
- (void)testExample {
NSString *wifiIP = [NetworkAddress getWifiIPAddress];
NSString *cellularIP = [NetworkAddress getCellularIPAddress];
XCTAssertGreaterThanOrEqual(wifiIP.length, 10);
XCTAssertNil(cellularIP,@"simulator without cellular");
NSMutableArray *name = [NetworkAddress resolveDomainName:@"hhtps://www.baidu.com"];
NSLog(@"ssss%@", name);
}
@end
然后我就写出了如上的测试case,然后跑完发现这个类的coverage 很高,高达77%
很爽,但是!!!!!!
这样的case 毫无意义,因为即使这个时候这些个方法在某个时刻被改变了,函数完全不是预期的了,这样的case 并不能检查出来问题。所以,一定要设置好固定的预期值,然后大部分(不要求全部)正常异常case都要有,这样的函数级别的UT才有意义,不然即使覆盖率高达100%也毫无意义。
这里就要说怎么写了:比如创建文件夹函数,就有正常的创建成功,创建非法路径和创建已经存在的路径这三个常规case,因为创建文件夹函数有返回布尔值,所以直接拿来断言就好。
NSString *pathname = @"md5";
NSString *errorPathname = @"";
BOOL creatFolderPath = [FileUtil createFolder:pathname];
XCTAssertTrue(creatFolderPath,@"create folder");
BOOL creatErrorFolderPath = [FileUtil createFolder:errorPathname];
XCTAssertFalse(creatErrorFolderPath,@"create folder");
BOOL creatExistsFolderPath = [FileUtil createFolder:pathname];
XCTAssertTrue(creatExistsFolderPath,@"create folder");
所以,case不仅仅是让代码在XCTest中被执行一边,而是具有校验意义的。