重要!
- 對于即將新增的功能函數,一定要先寫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 <XCTest/XCTest.h>
#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 工程那么隨意放置。
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中被執行一邊,而是具有校驗意義的。