iOS單元測試詳解

簡介

  • 測試目的:模擬多種可能性,減少錯誤,增強健壯性,提高穩定性。
  • 測試種類:在iOS中的通常分為單元測試和UI測試。
    • 單元測試(Unit Test):用來保證每一個類正常工作。
    • UI測試(UI Test):從業務層的角度保證各個業務可以正常工作。
  • 測試框架:除了XCode自帶的測試框架XCTest,還有以下列出的三方框架。

XCTest:同時支持單元測試和UI測試。
單元測試:
- Kiwi 老牌測試框架
- specta 另一個BDD優秀框架
- Quick 三個項目中Star最多,支持OC和Swift,優先推薦。

UI測試:
- KIF 基于XCTest的測試框架,調用私有API來控制UI,測試用例用Objective C或Swift編寫。

  • appium 基于Client – Server的測試框架。App相當于一個Server,測試代碼相當于Client,通過發送JSON來操作APP,測試語言可以是任意的,支持android和iOS。

本博文講述使用框架XCTest及Xcode工具進行單元測試,編寫測試代碼,以及如何讓你的代碼更容易單元測試(可能是重構代碼的不歸路)。

參考鏈接

官方文檔講述進行單元測試及UI測試詳細信息,對如何進行測試,及寫可測試代碼有簡單說明。
單元測試基礎的單元測試。
自動化測試講述UI測試及測試三方框架的使用,及寫可測試代碼策略。
更輕量的ViewController為測試ViewController做鋪墊。
測試ViewController測試ViewController及相應的輔助測試工具。

XCTest測試

1. 測試基礎
  • 測試就是的您編寫的測試代碼,可以執行您的應用程序和庫代碼,根據一系列期望進行判斷(使用斷言XCTAssert),導致通過或失敗的結果。對于性能測量測試,參考標準可能是期望一組代碼運行到完成的最長時間。

  • 所有軟件都是使用組合構建的;也就是說,較小的組件被布置在一起以形成具有更大功能的更大,更高級別的組件,直到滿足項目的目標和要求。良好的測試實踐是進行測試,涵蓋此組合的各個層次的功能。 XCTest允許您為任何級別的組件編寫測試。您可以定義什么構成組件進行測試 - 它可以是一個類中的方法或一組完成重要目的的方法。測試組件的行為應該是完全確定的;測試通過或失敗。

  • 項目組件的測試設計的基礎是測試驅動開發,它是編寫代碼的一種風格,您可以在編寫被測試代碼之前編寫測試邏輯。這種開發方法使您可以在實現代碼之前對需求和邊緣案例進行整理編寫。編寫測試后,您將開發您的被測試代碼,目的是通過測試。 當您修復錯誤時,您將添加確認錯誤已修復的測試。

  • 性能測試
    XCTest提供API來測量基于時間的性能,測試必須有一個評估基準, 評估基準是測試方法的十次運行中的平均時間與每次運行的標準偏差的組合。 低于評估基準或從運行到運行的變化太大的測試報告為失敗,否則就是成功。

注意:首次執行性能測量測試時,XCTest始終報告失敗,因為未設置評估基準。

  • UI測試:模擬用戶操作,進而從業務層面測試。

UI測試的工作原理是通過查詢應用程序的UI對象,合成事件并將其發送到這些對象,并提供豐富的api,使您能夠檢查UI對象的屬性和狀態,將其與預期狀態進行比較。UI測試使您能夠查找應用程序的UI并與其進行交互,以驗證UI元素的屬性和狀態。

UI測試包括UI錄制,是幫助你快速編寫UI測試的好方法。

  • 應用程序和庫測試
    應用程序測試:測試檢查應用程序中代碼的正確行為,例如計算器應用程序的算術運算示例。

    庫測試:檢查動態庫和框架中代碼的正確行為,而不管它們在應用程序的運行時間中的使用情況。 你可以創建單元測試去構建庫測試。

2. 測試從哪開始
  • 在創建單元測試時,專注于測試您的代碼的最基本的基礎,Model類和與Controller進行交互的方法。你的應用程序中很有可能有Model,View,Controller這些類,首先編寫測試來覆蓋所有的Model類時,確定Model或者更基礎的類是經過很好的測試,然后再開始為Controller類編寫測試,這些測試開始接觸應用程序的復雜部分,例如,連接到網絡。

  • 在創建UI測試時,首先考慮最常見的工作流程。想想用戶在開始使用應用程序時使用什么以及在該過程中立即執行什么。使用UI錄制功能是將用戶操作序列捕獲到UI測試方法中的好方法。

3. 新建測試Target 及 測試類
  • 新建工程選擇單元測試和UI測試


    step1. 新建工程選擇單元測試和UI測試

    左側Project navigation區和右側Project editor區中對應新建項目中的單元測試和UI測試的資源。


    step2. Project navigation & Project editor

    新建的測試Target默認包含一個測試類,這個類是XCTestCase的子類,且這個類只有.m文件。
    step3.
  • 選擇Project editor新建測試


    step1. Project editor中新建單元測試

    選擇需要測試的Target,新建單元測試


    step2. 選擇需要測試的Target
    • 選擇Test navigation新建測試,在這里可以選擇新建測試Target 或者測試類。你可以根據你的意愿新建任意多個測試Target,每個測試Target下可以新建任意多個測試類。


      step1.Test navigation新建測試或者測試類

Test navigation中分層顯示出項目中包含的測試Target,測試類,以及測試方法。


Test navigation
4. 編寫測試方法

測試方法是以前綴test開頭的測試類的實例方法,不需要參數,返回void,例如-(void)testExample()。測試方法在您的項目中執行代碼,使用XCTest框架提供的斷言來呈現Xcode顯示的測試結果。如果該代碼不產生預期結果,則使用一組斷言(XCTAssert)API報告失敗。例如,函數的返回值可能會與預期值進行比較。

  • 斷言的最后一個參數為失敗結果的描述字符串,該參數可選,可以不傳。一個測試方法可以包括多個斷言。 如果其中包含的任何斷言報告失敗,則Xcode將指示該測試方法失敗。使用斷言需要的注意是斷言的參數類型,可能是BOOL 類型,對象類型,基礎數據類型,可能是個表達式等。
-(void)testUnconditionalFail {
//1. Unconditional Fail:無條件失敗當直接到達特定的代碼分支指示失敗時使用。
       XCTFail(@"無條件失敗....");

//2.Boolean Tests
    BOOL a = NO;
    XCTAssert(a,@"失敗時提示:a == false");
    XCTAssertTrue(a,@"失敗時提示:a == false");
    XCTAssertFalse(a,@"失敗時提示:a == true");

//3.基礎數據類型
    NSInteger b = 1;
    NSInteger c = 1;
    NSInteger d = 2;
    XCTAssertEqual(b, c, @"失敗時提示:b!= c");
    XCTAssertGreaterThan(d, c,@"失敗時提示:d < c");
    XCTAssertEqualWithAccuracy(c, d, 1,@"失敗時提示:c和d的誤差的絕對值大于1");

//4.對象類型
    NSString *nameA = @"nameA";
    NSString *nameB = @"nameB";
    XCTAssertEqualObjects(nameA, nameB,@"失敗時提示:nameA != nameB");
    XCTAssertNil(nameA,@"失敗時提示:nameA != nil");
    
//5. Exception Tests
    NSArray *array = @[];
    XCTAssertThrows(array[0],@"失敗時提示:array[0]沒有拋出異常");
    XCTAssertNoThrow(array[0],@"失敗時提示:array[0]拋出異常");
    XCTAssertThrowsSpecific(array[0], NSException,@"失敗時提示:array[0]沒有拋出NSException異常");
    XCTAssertThrowsSpecificNamed(array[0], NSException,@"NSRangeException",@"失敗時提示:array[0]沒有拋出名為NSRangeException的NSException異常");
}
  • 當Xcode運行測試時,它會獨立地調用每個測試方法。因此,每個方法必須準備和清理任何輔助變量。如果此代碼適用于類中的所有測試方法,則可以將其添加到setUp和tearDown的實例方法中, 在每個測試方法之前和之后調用。
//
-(void)setUp {
    [super setUp];
    // 初始化代碼放在這里. 在調用這個類的每個測試方法之前都要調用.
}
//
-(void)tearDown {
    // 銷毀代碼放在這里. 在調用這個類的每個測試方法之后都要調用.
    [super tearDown];
}

您可以選擇添加到setUp和tearDown的類方法中,在類中的所有測試方法之前和之后運行。

//
+(void)setUp {
    [super setUp];
    // 初始化代碼放在這里. 在調用這個類的所有測試方法之前調用.
}
//
+(void)tearDown {
  // 銷毀代碼放在這里. 在調用這個類的所有測試方法之后調用.
    [super tearDown];
}

以下是MZBayTests測試類的基本結構

//
-(void)setUp {
    [super setUp];
    // 初始化代碼放在這里. 在調用這個類的每個測試方法之前都要調用.
}
//
-(void)tearDown {
    // 銷毀代碼放在這里. 在調用這個類的每個測試方法之后都要調用.
    [super tearDown];
}
//
-(void)testExample {
    //這是一個功能測試用例的例子。
   //使用XCTAssert和相關函數來驗證您的測試是否產生正確的結果。
}
//
-(void)testPerformanceExample {
  //這是一個性能測試用例的例子。
   [self measureBlock:^ {
       //把你想要測量運行的時間的代碼放在這里。
  }];
}

單元測試中包含普通測試,性能測試,異步測試。

  • 普通測試
- (void)testModelFunc_randomLessThanTen{
    
    Model * model = [[Model alloc] init];
    
    NSInteger num = [model randomLessThanTen];
    
    XCTAssert(num < 10,@"失敗時提示: num should less than 10");
}
  • 性能測試
-(void)testAdditionPerformance {

    [self measureBlock:^{
       //把你想要測量運行的時間的代碼放在這里。
        
          for (NSInteger index = 0; index < 100000; index ++) {
            NSString *str = [@((index+1) % 100) description];
        }

    }];
}
  • 異步測試
- (void)testAsyncFunction{
    //創建一個XCTestExpectation對象。
    //這個測試只有一個,可以等待多個XCTestExpectation對象。
    
    XCTestExpectation * expectation = [self expectationWithDescription:@"Just a demo expectation,should pass"];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
         NSLog(@"Async test");
        XCTAssert(YES,"should pass");
         //完成相應操作后調用fulfill  這將導致-waitForExpectation
        [expectation fulfill];
    });
    
     //測試將在此暫停,運行runloop,直到超時調用 或所有的expectations都調用了fulfill方法。
    [self waitForExpectationsWithTimeout:0.5 handler:^(NSError *error) {
        //Do something when time out關閉文件等操作
    }];
}
5. 運行測試及查看測試結果
  • 快捷鍵:Command + U 即Xcode菜單中 Product -> Test
    特點:會運行所有測試類中所有的測試方法。
  • Test navigation區:點擊測試類或者測試方法的右測運行按鈕
    點擊測試類右側運行按鈕:運行此測試類中的所有測試方法。
    點擊測試方法右側運行按鈕:運行此測試方法。
  • Source editor區:點擊測試方法或者測試類名字左側的運行按鈕。
  • 運行當前鼠標所在測試方法:control + option + command + U
  • Test again(運行剛剛運行過的測試方法):control + option + command + G
  • 修改MZBayTests類,添加以下三個測試方法。


    step1.MZBayTests類中的測試方法

    現在選擇Command + U運行測試,測試結果可以在Test Navigation和Source editor中直觀看到。綠色的鉤代表測試成功,紅色的叉表示測試失敗。普通測試和性能測試都測試成功,異步測試因為超時失敗了。

測試結果

點擊Source editor中testAdditionPerformance測試方法左側灰色圖標,進行最大標準差和baseline的設置。還可查看性能測試運行10次代碼所花費的時長。


step1.性能測試

輸入Max STDDEV(最大標準差)和baseline并保存。


step2.性能測試

在Reports navigation中,選中你要查看的測試即可查看更詳細的測試結果信息。
Reports navigation
  • Tests 用來查看詳細的測試過程
  • Coverage 用來查看代碼覆蓋率
  • Logs 用來查看測試的日志
6. 代碼覆蓋率

Xcode中的代碼覆蓋是LLVM提供的的測試選項。 當您啟用代碼覆蓋時,LLVM將根據調用方法和函數的頻率,對代碼收集覆蓋數據。 代碼覆蓋選項可以收集數據以報告正確性和性能的測試,無論是單元測試還是UI測試。

  • 在 scheme editor 菜單中選中 Edit Scheme .


    選中 Edit Scheme
  • 選中Test action,啟用代碼覆蓋復選框以收集覆蓋率數據。


    啟用代碼覆蓋率

    注意:代碼覆蓋率數據收集會導致性能損失。當啟動代碼覆蓋時,它以線性方式影響代碼的執行。當嚴格評估測試的性能時,應該考慮是否啟用代碼覆蓋。

Reports navigator中 Coverage 菜單中可以查看代碼覆蓋的相關數據。

Coverage

用鼠標選中 - [Calculator input:]方法,將顯示一個按鈕,點擊該按鈕將帶您進入帶注解的源代碼。


Coverage

Source editor顯示文件中每行代碼,在測試期間特定部分代碼被調用的次數的注解在Source editor右側繪制。


Coverage

并突出顯示未執行的代碼。 它突出了需要覆蓋的代碼領域,而不是已經涵蓋的領域。


Coverage
  • 代碼覆蓋率的意義在于告訴我們,運行測試時實際運行什么代碼?代碼的哪些部分未被測試?換句話說,是否已經設計了足夠的測試,以確保正在檢查您的所有代碼的正確性和性能?
7. 別人家的測試
  • FMDB
    創建一個基類FMDBTempDBTests,讓它實現創建數據庫,及關閉數據庫操作。
    其他測試類都繼承FMDBTempDBTests基類。按照功能劃分創建了多個測試類。
    FMDatabaseAdditionsTests對應著插入數據測試
    FMDatabaseQueueTests對應著多線程下進行數據庫操作測試。
  • FMDBTempDBTests.h

FMDBTempDBTests.m的實現


FMDBTempDBTests.m
  • +setUp: 創建了一個空的數據庫,然后調用實現的協議方法[self populateDatabase:db],進行創建表,并插入數據。
  • -setUp: 將+setUp創建好的數據庫拷貝到每個測試方法都使用的數據庫地址,保證每個測試方法使用的數據庫數據都是+setUp配置好的初始數據庫,沒有被別的測試方法污染。
  • -tearDown:關閉數據庫。

FMDBTempDBTests的子類只要實現populateDatabase:db方法就配置好測試所需要的數據,可以直接寫測試代碼了。


FMDatabaseQueueTests.m

編寫測試代碼

  • 定義API要求:添加到項目中的每個方法或函數定義需求和結果很重要。對于需求,包括輸入和輸出范圍,拋出的異常以及引發它們的條件以及返回的值的類型。

  • 在編寫代碼時編寫測試用例:在設計和編寫每個方法或函數時,請編寫一個或多個測試用例以確保滿足API的要求。

  • 檢查邊界條件。如果方法的參數必須具有特定范圍內的值,則測試應傳遞包含范圍的最低和最高值的值。例如,如果一個過程具有可以具有0到100之間的值的整數參數,則該方法的測試代碼應該為參數傳遞值0,50和100。

  • 使用負面測試。負面測試確保您的代碼適當地響應錯誤條件。驗證您的代碼在收到無效或意外輸入值時的行為是否正確。還要驗證它是否返回錯誤代碼或引發異常。例如,如果一個整數參數的值必須在0到100之間,包括值,則創建傳遞值為-1和101的測試用例,以確保該過程引發異常或返回錯誤代碼。

  • 編寫綜合測試用例。綜合測試結合不同的代碼模塊來實現一些更復雜的API行為。雖然簡單,孤立的測試提供價值,堆疊測試運行復雜的行為,并傾向于捕獲更多的問題。這些類型的測試在更現實的條件下模擬您的代碼的行為。例如,除了向數組添加對象之外,您還可以創建數組,向其中添加多個對象,使用不同的方法刪除其中的一些對象,然后確保剩余對象的集合和數量正確。

  • 用測試用例覆蓋您的錯誤修復。每當您修復錯誤時,請編寫一個或多個驗證修補程序的測試用例。

  • 測試用例分為三部分:

    1. 配置測試的初始狀態
    2. 對要測試的目標執行代碼
    3. 對測試結果進行斷言(成功 or 失敗)
  • 測試代碼結構
    當測試用例多了,你會發現測試代碼編寫和維護也是一個技術活。通常,我們會從幾個角度考慮:

    1. 不要測試私有方法(封裝是OOP的核心思想之一,不要為了測試破壞封裝)
    2. 對用例分組(功能,業務相似)
    3. 對單個用例保證測試獨立(不受之前測試的影響,不影響之后的測試),這也是測
      試是否準確的核心。
    4. 提取公共的代碼和操作,減少copy/paste這類工作。

讓你的代碼更容易單元測試

  • 通常,為了單元測試的準確性,一個方法對于同樣的輸入,輸出是一致的。如果你寫了一個沒有參數,或者沒有返回值的方法。那這個方法就很難測試了。

  • 如果項目框架使用的是MVC,
    View只做純粹的展示型工作,把用戶交互通過各種方式傳遞到外部
    Model只做數據存儲類工作
    Controller作為View和Model的樞紐,往往要和很多View和Model進行交互,也是測試的痛點。

  • 對Controller瘦身是iOS架構中比較重要的一環。可參考更輕量的ViewController

    1. 把 UITableViewDataSource 的代碼提取出來放到一個單獨的類中,你可以單獨測試這個類,可以復用,再也不用寫第二遍。也適用于其他的protocol,如UICollectionViewDataSource。
  1. 將業務邏輯移到 Model 中,查找一個用戶的目前的優先事項的列表,做為User的一個Category方法,而不是vc中直接寫。
  2. 創建Store類,Store 對象會關心數據加載、緩存和設置數據棧。它也經常被稱為服務層或者倉庫。
  3. 把網絡請求邏輯移到 Model 層,要在 view controller 中做網絡請求的邏輯。取而代之,你應該將它們封裝到另一個類中。這樣,你的 view controller 就可以在之后通過使用回調(比如一個 completion 的 block)來請求網絡了。這樣的好處是,緩存和錯誤控制也可以在這個類里面完成。
  4. 把View 代碼移到 View 層。不應該在 view controller 中構建復雜的 view 層次結構。你可以使用 Interface Builder 或者把 views 封裝到一個 UIView 子類當中。
  5. 通訊:其他在 view controllers 中經常發生的事是與其他 view controllers,model,和 views 之間進行通訊。這當然是 controller 應該做的,但我們還是希望以盡可能少的代碼來完成它。

這塊內容很空洞,可以參考更輕量的ViewController來做。

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

推薦閱讀更多精彩內容