學習本篇文章你可以收獲以下知識點:
1.單元測試簡介
2.蘋果自帶的XCTest
3.單元測試的適用情況以及覆蓋率
4.依賴注入
一.單元測試簡介
單元測試是指開發者編寫代碼,去驗證被測代碼是否正確的一種手段,其實就是用代碼去檢測代碼。合理的利用單元測試可以提高軟件的質量。
我是去年開始關注單元測試這一塊,并且在項目中一直在實踐。可能之前一直更關注功能的實現,后期的測試都交給了QA,但是總會有一些QA也遺漏掉的點,bug上線了簡直要gg。這里就對單元測試以及依賴注入做個總結,希望對大家有所幫助,提高你們項目的質量。
二.蘋果自帶的XCTest
蘋果在Xcode7中集成了XCTest單元測試框架,我們可以在新建工程的時候直接勾選,如下圖1。
新建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 && [response.age integerValue] <= 110 , @"姓名不合法---0<age<=110認為是合法年齡");
}
點擊運行- (void)testPersonalInformationAge方法左側的菱形圖標運行檢查當前方法,會發現運行成功提示,如下圖4。并且- (void)testPersonalInformationAge方法左邊的菱形圖標展示位綠顏色代表通過。
接下來我們將- (void)testPersonalInformationAge方法內容改為如下測試用例,再次點擊菱形圖標看看效果如下圖5。
- (void)testPersonalInformationAge
{
ResponsePersonalInformation * response = [[ResponsePersonalInformation alloc] init];
response.age = @"120"; //模擬非法合法年齡( 0<age<110認為是合法年齡)
[self checkAge:response];
}
控制臺打印:
Test Suite 'Selected tests' started at 2018-04-19 15:41:40.754
Test Suite '單元測試Tests.xctest' started at 2018-04-19 15:41:40.755
Test Suite 'PersonalInformationTests' started at 2018-04-19 15:41:40.755
Test Case '-[PersonalInformationTests testPersonalInformationAge]' started.
/Users/xulin/Desktop/單元測試/單元測試/PersonalInformationTests.m:34: error: -[PersonalInformationTests testPersonalInformationAge] : (([response.age integerValue] >0 && [response.age integerValue] <= 110) is true) failed - 姓名不合法---0<age<110認為是合法年齡
以上只是講解了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 2018-04-19 15:47:28.750
Test Suite '單元測試Tests.xctest' started at 2018-04-19 15:47:28.751
Test Suite 'PersonalInformationTests' started at 2018-04-19 15:47:28.751
Test Case '-[PersonalInformationTests testPerformanceExample]' started.
/Users/xulin/Desktop/單元測試/單元測試/PersonalInformationTests.m:50: Test Case '-[PersonalInformationTests testPerformanceExample]' measured [Time, seconds] average: 0.007, relative standard deviation: 22.585%, values: [0.006975, 0.005990, 0.005411, 0.005087, 0.006447, 0.005386, 0.006691, 0.009266, 0.009946, 0.006949], performanceMetricID:com.apple.XCTPerformanceMetric_WallClockTime, baselineName: "", baselineAverage: , maxPercentRegression: 10.000%, maxPercentRelativeStandardDeviation: 10.000%, maxRegression: 0.100, maxStandardDeviation: 0.100
我們可以從中獲取到最有價值的信息: measured [Time, seconds] average: 0.002, relative standard deviation: 4.830%, values: [0.006975, 0.005990, 0.005411, 0.005087, 0.006447, 0.005386, 0.006691, 0.009266, 0.009946, 0.006949],從這里我們可以獲知在一個for循環重復的代碼,程序會運行10次,取一個平均運行時間值,average: 0.007這個就是平均時間0.007秒。
現在我們知道了測量一個函數的運行時間,到底這個函數效率高不高可以使用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%。
四.依賴注入
依賴注入是一種分離依賴,減少耦合的技術。可以幫助寫出可維護,可測試的代碼。那么為什么這里要提到依賴注入呢?因為在我們的單元測試中,某些時候模塊間的依賴太高,會影響我們的單元測試,合理使用依賴注入幫助我們進行測試。
先舉一個:
#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 。關于依賴注入這里就簡單介紹一下,如果大家還有興趣可以繼續查閱相關依賴注入的相關文檔。