【SDK】XCTest 编写过程中的一些问题和技巧

重要!

  1. 对于即将新增的功能函数,一定要先写UT再写实现代码。因为这样可以验收代码设计的角度避免一些思路问题。这一点很重要,也很好理解,资料很多就不赘述了。
  2. 对于已有的代码,后面补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种处理方式:
  1. 直接删除这个没有用到的方法
  2. 继续写UT

这里我的思路是这样的:对于SOLID里的S原则来说,文件读写类必须有增删改查四个功能,即使此时用不到,如果少了查询功能,那么你的功能函数是不完整的。一个函数干一个事,一个功能必须四个干事的函数。你现在用不到,可能你的同时负责的功能能用到呢,或者你以后能用到呢。所以这里是要写UT的。


  • 遇到压根就不用的类怎么办(可能以前用,现在不用了。或者从来就没有用过)
    其实这个就不是UT的范畴了,这里和同事商量了下:在写UT初期,先跳过这样的类。加个TODO标记下。

我一共尝试了两种方式

  1. 从业务角度出发:
    从你业务最开始的逻辑部分开始,一层层剥离开,然后去到那个单一功能类,比如文件管理类,然后针对该类的函数写UT Case

2.从代码结构出发,一般项目都会有Utils Handler Http这样的底层代码文件夹。直接在UT文件夹下建立类似的文件目录,方便管理测试代码。而不是UT demo 工程那么随意放置。


4601578974308_.pic.jpg
第一种不可取,因为业务逻辑没个定型,刚开始还好,然后看着看着就不知道去哪里了,迷失了自我。第二种则思路比较清晰,也能客观的看这个类的封装及功能设计是否合理。

  • 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%
很爽,但是!!!!!!


30051578985283_.pic.jpg

这样的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中被执行一边,而是具有校验意义的。

你可能感兴趣的:(【SDK】XCTest 编写过程中的一些问题和技巧)