淺談iOS單元測試

學習本篇文章你可以收獲以下知識點:
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 && [response.age integerValue] <= 110 , @"姓名不合法---0<age<=110認為是合法年齡");
}

點擊運行- (void)testPersonalInformationAge方法左側的菱形圖標運行檢查當前方法,會發現運行成功提示,如下圖4。并且- (void)testPersonalInformationAge方法左邊的菱形圖標展示位綠顏色代表通過。


圖4.檢查通過

接下來我們將- (void)testPersonalInformationAge方法內容改為如下測試用例,再次點擊菱形圖標看看效果如下圖5。

- (void)testPersonalInformationAge
{
    ResponsePersonalInformation * response = [[ResponsePersonalInformation alloc] init];
    response.age = @"120";   //模擬非法合法年齡( 0<age<110認為是合法年齡)
    [self checkAge:response];
}
圖5.檢查不通過

控制臺打印:

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%。


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中還有一些優秀的依賴注入開源框架,比如ObjectionTyphoon 。關于依賴注入這里就簡單介紹一下,如果大家還有興趣可以繼續查閱相關依賴注入的相關文檔。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 文章來自:http://blog.csdn.net/mj813/article/details/52451355 ...
    好大一只鵬閱讀 9,215評論 2 126
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,881評論 18 139
  • 單元測試不是一個小工程,需要多用些時間才能做好,不要希望通過這個文章就能掌握單元測試,這只是一個入門,需要自己動手...
    勇不言棄92閱讀 7,881評論 9 60
  • 前言 如果有測試大佬發現內容不對,歡迎指正,我會及時修改。 大多數的iOS App(沒有持續集成)迭代流程是這樣的...
    默默_David閱讀 1,696評論 0 4
  • 代碼質量的重要性不言而喻,直接影響了項目質量和團隊開發效率,對于如何提高代碼的質量,除了依賴開發人員本身的技術素質...
    點融黑幫閱讀 3,648評論 0 15