iOS 之性能測試、UI測試

測試時從何處開始

當我們開始測試時,應當遵守以下原則:

  • 當創建單元測試時,應當著重于代碼中最基礎的部分,即與 Controller交互的Model類和方法。
  • 當創建UI測試時,首先考慮最常見的工作流程。想象下用戶開始使用app時會做些什么以及過程中執行了哪些UI。使用UI recording 功能可以捕捉用戶的一系列動作到測試方法中,也可以擴展該方法來驗證測試的正確性或性能。

創建一個測試類

我們可以使用導航欄下方的加號按鈕創建一個新的測試類

test-0
test-1.png

也可以使用command + N的快捷鍵方式來創建

test-2

上圖選擇的分別的UI測試和單元測試。

注意: 所有的測試類都是XCTest框架的XCTestCase的子類

測試類的結構

測試類的結構如下:

#import <XCTest/XCTest.h>
 
@interface SampleCalcTests : XCTestCase
@end
 
@implementation SampleCalcTests
 
- (void)setUp {
    [super setUp];
    // 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.
    [super tearDown];
}
 
- (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.
    [self measureBlock:^{
        // Put the code you want to measure the time of here.
    }];
}
@end

該實現包含setupteardown實例方法的基本實現,但這些方法并不是必需的。如果所有的測試方法使用了相同的代碼,我們可以把這些代碼放在setupteardown中。

一般在setup方法中,進行初始化操作、代碼復用和準備測試條件。
tearDown方法中,一般會進行釋放對象,以避免干擾和回收資源。

測試執行的順序

默認情況下,當運行測試時,XCTest會查詢所有的測試類,并執行每個測試類中所有的測試方法。

對于每個類,都是以setup類方法開始的。對于每個測試方法,都會創建一個新的類的實例并執行setup實例方法。然后執行測試方法,再然后執行teardown實例方法。類中每個測試方法都是如此。當類中最后一個測試方法的teardown方法運行之后,Xcode會執行teardown類方法并移到下一個類中。重復該動作直到所有的測試類都運行完畢。

執行順序

測試方法的書寫

測試方法是一個以test開頭,無參無返回的實例方法。如果測試方法中沒有達到我們期望的效果,可以使用一組斷言來報告失敗。

當 Xcode 運行測試時,它會獨立調用每個測試方法。因此,每個方法必須準備和清除任何它需要與主題API交互的輔助變量,結構體和對象。如果類中所有的測試方法都有共有的代碼,可以把這些代碼添加到setuptearDown實例方法中。

下面就是一個單元測試方法:

- (void)testColorIsRed {
   // Set up, call test subject API. (Code could be shared in setUp method.)
   // Test logic and values, assertions report pass/fail to testing framework.
   // Tear down. (Code could be shared in tearDown method.
}

運行測試方法

測試方法的運行有多種類型

run test1
  • 將鼠標放在導航欄上,也就是圖中 1的位置,將會運行bundle 中所有的測試方法
  • 將鼠標放在類中的運行按鈕處,即圖中 2的位置,將運行類中所有的測試方法
  • 將鼠標放在方法右方的運行按鈕處,即 圖中 3位置處,將只運行該測試方法

或者在類文件中也可以,如下圖:


run test 2

測試方法通過將會顯示圖中的綠色標記,不通過將會出現紅色圖標,如圖所示:


run test fail

如果要運行工程中所有的測試方法,可以選中 Product > Test

運行所有的測試方法

下面的按鈕會只顯示測試失敗的方法

show only fail

異步操作測試

測試是同步執行的,因為每個測試方法都是獨立調用的。但越來越多的代碼執行是異步的。為了處理調用異步執行方法的組件和函數,XCTest在Xcode 6中進行了加強,包括通過等待異步回調或超時完成之后,在測試方法中序列化異步執行的

文檔中提供的例子:

// Test that the document is opened. Because opening is asynchronous,
// use XCTestCase's asynchronous APIs to wait until the document has
// finished opening.
- (void)testDocumentOpening
{
    // Create an expectation object.
    // This test only has one, but it's possible to wait on multiple expectations.

    // 創建一個expectation 對象 這里只創建了一個,但是也可以創建多個expectations

    XCTestExpectation *documentOpenExpectation = [self expectationWithDescription:@"document open"];
 
    NSURL *URL = [[NSBundle bundleForClass:[self class]]
                              URLForResource:@"TestDocument" withExtension:@"mydoc"];
    UIDocument *doc = [[UIDocument alloc] initWithFileURL:URL];
    [doc openWithCompletionHandler:^(BOOL success) {
        XCTAssert(success);
        // Possibly assert other things here about the document after it has opened...
 
        // Fulfill the expectation-this will cause -waitForExpectation
        // to invoke its completion handler and then return.

        // 實現該expectation后會調用  -waitForExpectation 方法的  handler block
        [documentOpenExpectation fulfill];
    }];
 
    // The test will pause here, running the run loop, until the timeout is hit
    // or all expectations are fulfilled.

    // 測試方法將在這暫停, 運行 runloop, 直到 超過`timeout`時間或者所有的 expectation 都實現了
    [self waitForExpectationsWithTimeout:1 handler:^(NSError *error) {
        [doc closeWithCompletionHandler:nil];
    }];
}

上面代碼就是在文檔打開之后(調用了fulfill方法)或者超過 1s(timeout值)還沒打開的話會執行 handler塊內的內容。

XCTestExpectation代表了異步測試中的特定條件。- fulfill方法標記該期望被滿足。

- (void)waitForExpectationsWithTimeout:(NSTimeInterval)timeout handler:(nullable XCWaitCompletionHandler)handler該方法會在expectation滿足或者在timeout時間后執行handler塊。

重點:

該方法僅等待 XCTestCase的便利方法創建的expectation。不會等待XCTestExpectation或其子類上的初始化程序手動創建的expectation值。(這里我們創建expectation時的self是指XCTestCase實例,所以會等待)

要等待手動創建的expectation,使用waitForExpectations:timeout:waitForExpectations:timeout:enforceOrder:方法或XCTWaiter上的相應方法,傳遞一個明確的expectation列表。

性能測試的書寫

性能測試會運行想要評估的代碼塊十次,收集平均執行時間和運行的標準偏差。然后平均值與baseLine進行比較以評估成功或失敗。

baseLine是我們指定的用來評估測試通過或者失敗的值。我們也可以自己指定一個特定的值。

自定義BaseLine

要實現性能測試,我們可以使用Xcode 6之后的新的API。

- (void)testPerformanceExample {
    // This is an example of a performance test case.
    //  這是一個性能測試的例子
    [self measureBlock:^{
        // Put the code you want to measure the time of here.
        // 把想要測試的代碼放在這里
    }];
}

下面的簡單示例顯示了一個性能測試,用于測試計算器示例應用程序的速度

- (void) testAdditionPerformance {
    [self measureBlock:^{
        // set the initial state
        [calcViewController press:[calcView viewWithTag: 6]];  // 6
        // iterate for 100000 cycles of adding 2
        for (int i=0; i<100000; i++) {
           [calcViewController press:[calcView viewWithTag:13]];  // +
           [calcViewController press:[calcView viewWithTag: 2]];  // 2
           [calcViewController press:[calcView viewWithTag:12]];  // =
        }
    }];
}

UI 測試

UI測試能夠查找應用程序的UI并與其進行交互,以驗證屬性和UI元素的狀態。

UI測試包括UI recording,能夠生成代碼,這些代碼可以像用戶一樣執行應用程序的用戶界面,并且可以擴展實現UI測試。這是快速開始編寫UI測試的好方法。

UI測試在基本原理方面與單元測試不同。單元測試允許你在app范圍內工作,并允許我們通過完全訪問應用程序的變量和狀態來執行函數和方法。UI測試是以用戶相同的方式來操作app的UI,(不會訪問app內的方法函數和變量)。這使測試能夠以與用戶相同的方式查看app,從而暴露用戶遇到的UI問題。

APIs

UI測試給予下面三個類的實現:

  • XCUIApplication

  • XCUIElement

  • XCUIElementQuery

UI recording

UI recording 會在測試方法中生成代碼,可以對其進行編輯以構建測試或回放特定的使用場景。UI recording 對探索新的UI或學習如何編寫UI測試序列也很有用。操作的基本順序是:

  1. 把鼠標放到方法里

  2. 點擊紅色錄制按鈕,開始記錄UI。應用程序會啟動,這是我們可以點擊UI元素(模擬用戶行為),鼠標處就會自動生成代碼


    UI recording 開始按鈕
  3. 完成錄制之后,點擊停止錄制按鈕

    停止錄制按鈕
  4. 可以加入我們自己想要的代碼邏輯,比如加入XCTest斷言

UI測試正確性的一般模式是:

  1. 使用 XCUIElementQuery 查找 XCUIElement
  2. 合成一個事件并把它發送給XCUIElement
  3. 使用斷言來比較 XCUIElement 的狀態和預期的參考狀態

比如,

- (void)testExample {
    // Use recording to get started writing UI tests.
    // Use XCTAssert and related functions to verify your tests produce the correct results.
    
    XCUIApplication *app = [[XCUIApplication alloc] init];
    XCUIElement *loginElement = [app.otherElements containingType:XCUIElementTypeButton identifier:@"login"].element;
    [loginElement twoFingerTap];
    
    XCUIElement *textField = [[app.otherElements containingType:XCUIElementTypeButton identifier:@"login"] childrenMatchingType:XCUIElementTypeTextField].element;
    [textField tap];
    [textField tap];
    [textField tap];
    [textField tap];
    [textField typeText:@"1234"];
    [textField typeText:@"5678"];
    [textField typeText:@"9010"];
    [textField typeText:@"5201"];
}

運行之后:

UI-recording.gif

會自動執行方法里的內容,執行里面的操作(輸入文字和點擊按鈕)

XCTest 斷言

斷言分為五種類型

  • Unconditional Fail (無條件失敗). 當到達特定的代碼分支指示失敗時使用該斷言。這個類型中只有一個斷言:XCTFail.

  • Equality Tests. 斷言兩個item之間的關系。比如,XCTAssertEqual 聲明兩個表達式具有相同的值,同時XCTAssertEqualWithAccuracy聲明兩個表達式在一定的準確度內具有相同的值。此類別還包括不等測試,例如XCTAssertNotEqual 和 XCTAssertGreaterThan。

  • Boolean Tests. 斷言布爾表達式,比如 XCTAssertTrue 或者 XCTAssertFalse。

  • Nil Tests. 斷言一個item是否為nil. 比如 XCTAssertNil 或 XCTAssertNotNil 。

  • Exception Tests (異常測試). 斷言一個表達式是否會生成異常,使用 XCTAssertThrows 來拋出異常,也可以使用XCTAssertThrowsSpecific 指定特定的異常,也可以使用 XCTAssertNoThrow 來斷言表達式沒有異常。

代碼覆蓋率

代碼覆蓋率是Xcode 7中的新特性,允許我們可視化并測量代碼的執行的程度。通過代碼覆蓋率,可以確定測試是否正在我們想要的工作。

啟用代碼覆蓋

代碼覆蓋是LLVM支持的測試選項。當啟用代碼覆蓋,LLVM會根據方法和函數調用的頻率來檢測代碼以收集覆蓋率數據。代碼覆蓋選項可以收集數據以報告測試的正確性和性能,無論是單元測試還是UI測試。

  1. 選中 edit scheme選項(除了下圖的方式,還可以通過Product -> Scheme -> Edit Scheme
    F535F6A5-D7A0-4D09-B4DA-DC069941611A.png
  1. 選中Test -> Options -> Code Converage選項
D9BCB122-3BBA-4723-AD87-CE1908F88BEB.png

注意: 代碼覆蓋數據收集會導致性能損失。 無論該損失是否重要,它都會以線性方式影響代碼的執行,所以當啟用時,性能結果在測試運行之間保持可比性。但是,當你嚴格評估性能時需要考慮是否啟用代碼覆蓋

代碼覆蓋如何適用于測試

代碼覆蓋率是一個衡量測試價值的工具。它回答了以下問題:

  • 當你運行測試的時候實際運行的代碼是什么?
  • 有多少測試才足夠?
    換句話說,你是否構建了足夠多的測試來確保所有的代碼都得到了正確性和性能的檢查?
  • 代碼中的那些部分沒有被測試到?

測試運行完成后,Xcode 將獲取LLVM覆蓋率數據,并使用它創建一個覆蓋率報告。該報告展示了測試運行的主要信息、原文件的列表、文件中的函數以及 每個文件的覆蓋百分比

按下圖所示,可以查看代碼覆蓋率:


Reports navigator.png

下面是AFNetworking 的代碼覆蓋率截圖


AFNetworking 的代碼覆蓋率

點擊現實的按鈕或者雙擊,可以跳轉到源代碼

跳轉到源代碼

比如我們跳轉到了AFAutoPurgingImageCacheTests文件的testThatImagesArePurgedWhenCapcityIsReached方法。右側顯示的是覆蓋范圍的注釋,顯示了測試過程中特定代碼的執行次數。

執行次數

比如上圖的 最上方的 1 指的是該方法執行了一次, 數字11 指的是while循環執行了11次,其他數字同理。

同樣的,如果某個方法沒有被執行,它的數字就是 0了,

0次執行

(這里我是單獨運行了testThatImagesArePurgedWhenCapcityIsReached方法,所以上圖所示的方法當然是運行0次了)

最后

Xcode對測試的集成支持使您能夠編寫測試以各種方式支持您的開發工作。您我們可以使用測試來檢測代碼中的潛在問題,發現是否符合預期,并驗證應用程序的行為,提高代碼的穩定性。

當然,通過測試獲得的穩定水平取決于編寫的測試代碼的質量。同樣,編寫好測試的難易程度取決于編寫代碼的方式。閱讀以下指導原則以確保您的代碼是可測試的,并且可以簡化編寫良好測試的過程。

  • 定義API的要求. 定義添加到項目中的每種方法或功能的需求和結果非常重要。對于需求,包括輸入和輸出范圍,拋出的異常和引發它們的條件以及返回值的類型。指定需求并確定代碼中需求已經達到可以使我們的代碼更加安全強壯。

  • 編寫代碼時編寫測試用例. 在設計和編寫每個方法和函數時,編寫一個或多個測試用例以確保符合API的需求。為現有代碼編寫測試比在編寫代碼時就寫測試更困難

  • 檢查邊界條件. 如果方法的參數必須具有特定范圍內的值,則測試應傳遞包含范圍的最低和最高值的值。

  • 使用negative test(負面測試). negative test 可確保我們的代碼適當地響應錯誤條件。驗證我們的代碼在接收到無效或意外的輸入值時行為正確。還應驗證它是否返回錯誤代碼或引發異常。例如,如果一個整數參數必須是在0到100范圍內,測試用例值傳遞-1和101以確保該過程引發一個異常或返回錯誤代碼。


大家也可以查看蘋果的 官方文檔

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

推薦閱讀更多精彩內容