iOS開發—單元測試和UI測試教程

語言: CN / TW / HK

theme: scrolls-light

弄清楚要測試什麼

在編寫任何測試之前,瞭解基礎知識很重要。你需要測試什麼?

如果您的目標是擴充套件現有應用程式,您應該首先為您計劃更改的任何元件編寫測試。

通常,測試應涵蓋:

  • 核心功能:模型類和方法及其與控制器的互動
  • 最常見的 UI 工作流程
  • 邊界條件
  • Bug修復

瞭解測試的最佳實踐

首字母縮略詞FIRST描述了一組簡潔的有效單元測試標準。這些標準是:

  • 快速:測試應該快速執行。
  • 獨立/隔離:測試不應該相互共享狀態。
  • 可重複:每次執行測試時都應該獲得相同的結果。外部資料提供者或併發問題可能會導致間歇性故障。
  • 自我驗證:測試應該是完全自動化的。輸出應該是“通過”或“失敗”,而不是依賴於程式設計師對日誌檔案的解釋。
  • 及時:理想情況下,您應該在編寫測試的生產程式碼之前編寫測試。這被稱為測試驅動開發。

遵循 FIRST 原則將使您的測試保持清晰和有用,而不是成為應用程式的障礙。

01.png

Xcode 中的單元測試

測試導航器提供了使用測試的最簡單方法。您將使用它來建立測試目標並針對您的應用執行測試。

建立單元測試目標

開啟BullsEye專案並按Command-6開啟測試導航器。

單擊左下角的+ ,然後從選單中選擇New Unit Test Target... :

02.png

接受預設名稱BullsEyeTests並輸入com.raywenderlich作為組織識別符號。當測試包出現在測試導航器中時,通過單擊顯示三角形將其展開,然後單擊BullsEyeTests以在編輯器中開啟它。

03.png

預設模板匯入測試框架XCTest,並定義、with和示例測試方法的BullsEyeTests子類。XCTestCase``setUpWithError()``tearDownWithError()

您可以通過三種方式執行測試:

  1. 產品 ▸ 測試Command-U。這兩個都執行所有測試類。
  2. 單擊測試導航器中的箭頭按鈕。
  3. 單擊裝訂線中的菱形按鈕。

04.png

您還可以通過單擊測試導航器或裝訂線中的菱形來執行單個測試方法。

嘗試不同的方法來執行測試,以瞭解它需要多長時間以及它的外觀。樣本測試還沒有做任何事情,所以它們執行得非常快!

當所有測試都成功後,菱形將變為綠色並顯示覆選標記。單擊末尾的灰色菱形testPerformanceExample()開啟效能結果:

05.png

本教程不需要testPerformanceExample()或不需要testExample(),因此請刪除它們。

使用 XCTAssert 測試模型

首先,您將使用XCTAssert函式來測試 BullsEye 模型的核心功能:是否BullsEyeGame正確計算了一輪得分?

BullsEyeTests.swift中,在下面新增這一行import XCTest

@testable 匯入BullsEye

這使單元測試可以訪問 BullsEye 中的內部型別和函式。

在 的頂部BullsEyeTests,新增此屬性: var sut: BullsEyeGame!

這將建立一個佔位符BullsEyeGame,即被測系統(SUT),或此測試用例類與測試相關的物件。

接下來,將 的內容替換為setUpWithError()

嘗試 超級.setUpWithError() sut = BullsEyeGame ()

這會BullsEyeGame在類級別建立,因此該測試類中的所有測試都可以訪問 SUT 物件的屬性和方法。

在你忘記之前,釋放你的 SUT 物件tearDownWithError()。將其內容替換為:

sut = nil 嘗試 超級.tearDownWithError()

注意:最好在其中建立 SUTsetUpWithError()並將其釋放,tearDownWithError()以確保每次測試都以乾淨的狀態開始。如需更多討論,請檢視Jon Reid關於該主題的帖子。

編寫你的第一個測試

現在您已準備好編寫您的第一個測試!

將以下程式碼新增到末尾BullsEyeTests以測試您是否計算了猜測的預期分數:

``` func testScoreIsComputedWhenGuessIsHigherThanTarget () { // 給 定letguess = sut.targetValue + 5

// 什麼時候 sut.check(猜測:猜測)

// 然後 XCTAssertEqual (sut.scoreRound, 95 , "從猜測計算的分數是錯誤的" ) } ```

測試方法的名稱總是以test開頭,後跟對其測試內容的描述。

將測試格式化為givenwhenthen部分是一種很好的做法:

  1. Given:在這裡,您可以設定所需的任何值。在此示例中,您建立了一個guess值,以便您可以指定它與targetValue.
  2. 何時:在本節中,您將執行正在測試的程式碼:呼叫check(guess:)
  3. 然後: 這是您將通過在測試失敗時列印的訊息斷言您期望的結果的部分。在這種情況下,sut.scoreRound應該等於 95,因為它是 100 - 5。

通過單擊裝訂線或測試導航器中的菱形圖示執行測試。這將構建並執行應用程式,菱形圖示將變為綠色複選標記!您還會在 Xcode 上看到一個短暫的彈出視窗,它也表示成功,如下所示:

06.png

注意:要檢視XCTestAssertions的完整列表,請轉到Apple 的 Assertions Listed by Category

除錯測試

有一個故意內建的錯誤BullsEyeGame,您現在將練習查詢它。要檢視實際中的錯誤,您將建立一個從給定部分中減去 5 的測試,並使其他所有內容保持不變。targetValue

新增以下測試:

``` func testScoreIsComputedWhenGuessIsLowerThanTarget () { // 給 定letguess = sut.targetValue - 5

// 什麼時候 sut.check(猜測:猜測)

// 然後 XCTAssertEqual (sut.scoreRound, 95 , "從猜測計算的分數是錯誤的" ) } ```

guess和之間的差targetValue仍然是 5,所以分數應該仍然是 95。

在 Breakpoint 導航器中,新增一個Test Failure Breakpoint。當測試方法釋出失敗斷言時,這會停止測試運​​行。

07.png

執行你的測試,它應該在XCTAssertEqual測試失敗的那一行停止。

檢查sutguess在除錯控制檯中:

08.png

guesstargetValue − 5但是scoreRound是 105,而不是 95!

要進一步調查,請使用正常的除錯過程:在when語句中設定斷點,並在BullsEyeGame.swift的 inside中設定斷點check(guess:),它會在其中建立difference. 然後,再次執行測試,並跳過let difference語句以檢查difference應用程式中的值:

09.png

問題是difference負數,所以分數是 100 - (-5)。要解決此問題,您應該使用的絕對值differencecheck(guess:)中,取消註釋正確的行並刪除不正確的行。

刪除兩個斷點並再次執行測試以確認它現在成功。

使用 XCTestExpectation 測試非同步操作

現在您已經瞭解瞭如何測試模型和除錯測試失敗,是時候繼續測試非同步程式碼了。

BullsEyeGame用於URLSession獲取一個隨機數作為下一場比賽的目標。URLSession方法是非同步的:它們立即返回,但直到稍後才完成執行。要測試非同步方法,請使用XCTestExpectation讓您的測試等待非同步操作完成。

非同步測試通常很慢,因此您應該將它們與更快的單元測試分開。

建立一個名為BullsEyeSlowTests的新單元測試目標。開啟全新的測試類並在現有語句下方BullsEyeSlowTests匯入BullsEye應用程式模組:import @testable 匯入BullsEye

該類中的所有測試都使用預設URLSession傳送請求,因此sut在中宣告、建立setUpWithError()和釋放tearDownWithError()。為此,請將以下內容替換為BullsEyeSlowTests

``` var sut: URLSession!

覆蓋 func setUpWithError () throws { try super .setUpWithError() sut = URLSession(配置:.default) }

覆蓋 func tearDownWithError ()丟擲{ sut = nil 嘗試 超級.tearDownWithError() } ```

接下來,新增這個非同步測試:

``` // 非同步測試:成功快,失敗慢 func testValidApiCallGetsHTTPStatusCode200 () throws { // 給定 let urlString =
"http://www.randomnumberapi.com/api/v1.0/random?min=0&max=100&count=1" let url = URL(字串:urlString)! // 1 let promise = expect(description: "狀態碼: 200" )

// 當 let dataTask = sut.dataTask(with: url) { _ , response, error in // then if let error = error { XCTFail ( "Error: (error.localizedDescription) " ) return } else if let statusCode =(響應為? HTTPURLResponse)?.statusCode { if statusCode == 200 { // 2 promise.fulfill() } else { XCTFail ( "狀態碼: (statusCode) " ) } } } 資料任務.resume() // 3 等待(for: [promise], timeout: 5 ) } ```

此測試檢查傳送有效請求是否返回 200 狀態程式碼。大多數程式碼與您在應用程式中編寫的程式碼相同,只是添加了以下幾行:

  1. 期望(描述:):返回XCTestExpectation,儲存在promisedescription描述您期望發生的事情。
  2. promise.fulfill():在非同步方法的完成處理程式的成功條件閉包中呼叫它以標記已滿足期望。
  3. wait(for:timeout:):保持測試執行,直到滿足所有期望或timeout間隔結束,以先發生者為準。

執行測試。如果您已連線到 Internet,則在模擬器中載入應用程式後,測試應該需要大約一秒鐘才能成功。

快速失敗

失敗是痛苦的,但它不必永遠持續下去。

要體驗失敗,只需將 URL 更改testValidApiCallGetsHTTPStatusCode200()為無效的 URL:

讓url = URL(字串:“http://www.randomnumberapi.com/test”)!

執行測試。它失敗了,但它需要完整的超時間隔!這是因為您假設請求總是會成功,這就是您呼叫promise.fulfill(). 由於請求失敗,它僅在超時到期時才完成。

您可以通過更改假設來改進這一點並讓測試更快地失敗。與其等待請求成功,不如等待非同步方法的完成處理程式被呼叫。一旦應用程式收到來自伺服器的響應(OK 或錯誤),就會發生這種情況,這滿足了預期。然後您的測試可以檢查請求是否成功。

要檢視其工作原理,請建立一個新測試。

但首先,通過撤消您對url.

然後,將以下測試新增到您的類中:

``` func testApiCallCompletes () throws { // 給定 let urlString = "http://www.randomnumberapi.com/test" let url = URL (string: urlString) ! 讓promise =期望(描述:“呼叫完成處理程式”) var statusCode:Int? var responseError:錯誤?

// 當 let dataTask = sut.dataTask(with: url) { _ , response, error in statusCode = (response as? HTTPURLResponse ) ? .statusCode 響應錯誤=錯誤 promise.fulfill() } 資料任務.resume() 等待(為:[承諾],超時:5)

// 然後 XCTAssertNil (responseError) XCTAssertEqual (statusCode, 200 ) } ```

關鍵區別在於,只需輸入完成處理程式即可滿足預期,而這隻需要大約一秒鐘的時間。如果請求失敗,則then斷言失敗。

執行測試。現在應該大約需要一秒鐘才能失敗。它失敗是因為請求失敗,而不是因為測試執行超出timeout

修復url然後再次執行測試以確認它現在成功。

有條件地失敗

在某些情況下,執行測試沒有多大意義。例如,在testValidApiCallGetsHTTPStatusCode200()沒有網路連線的情況下執行會發生什麼?當然,它不應該通過,因為它不會收到 200 狀態碼。但它也不應該失敗,因為它沒有測試任何東西。

幸運的是,Apple 引入XCTSkip了在先決條件失敗時跳過測試。在 的宣告下方新增以下行sut

讓networkMonitor = NetworkMonitor .shared

NetworkMonitorwraps NWPathMonitor,提供了一種方便的方式來檢查網路連線。

在中,在測試的開頭testValidApiCallGetsHTTPStatusCode200()新增:XCTSkipUnless

試試 XCTSkipUnless ( networkMonitor.isReachable, “此測試需要網路連線。” )

XCTSkipUnless(_:_:)當沒有網路可達時跳過測試。通過禁用網路連線並執行測試來檢查這一點。您將在測試旁邊的裝訂線中看到一個新圖示,表示該測試既沒有通過也沒有失敗。

10.png

再次啟用您的網路連線並重新執行測試以確保它在正常情況下仍然成功。將相同的程式碼新增到testApiCallCompletes().

偽造物件和互動

非同步測試讓您確信您的程式碼會為非同步 API 生成正確的輸入。您可能還想測試您的程式碼在接收來自 的輸入時是否正常工作URLSession,或者它是否正確更新了UserDefaults資料庫或 iCloud 容器。

大多數應用程式與系統或庫物件互動 - 您無法控制的物件。與這些物件互動的測試可能很慢且不可重複,違反了FIRST原則中的兩個。相反,您可以通過從存根獲取輸入或更新模擬物件來偽造互動。

當您的程式碼依賴於系統或庫物件時,請使用偽造。通過建立一個假物件來扮演該角色並將這個假物件注入到您的程式碼中來做到這一點。Jon Reid 的Dependency Injection描述了幾種方法來做到這一點。

從存根偽造輸入

現在,檢查應用程式getRandomNumber(completion:)是否正確解析了會話下載的資料。您將BullsEyeGame使用存根資料偽造會話。

轉到 Test navigator,單擊+並選擇New Unit Test Class ...。將其命名為BullsEyeFakeTests,將其儲存在BullsEyeTests目錄中並將目標設定為BullsEyeTests

11.png

import在語句下方匯入 BullsEye 應用程式模組:

@testable 匯入BullsEye

現在,將 的內容替換為BullsEyeFakeTests

``` var sut: BullsEyeGame!

覆蓋 func setUpWithError () throws { try super .setUpWithError() sut = BullsEyeGame () }

覆蓋 func tearDownWithError ()丟擲{ sut = nil 嘗試 超級.tearDownWithError() } ```

這聲明瞭 SUT,即在BullsEyeGame中建立它並在 中setUpWithError()釋放它tearDownWithError()

BullsEye 專案包含支援檔案URLSessionStub.swift。這定義了一個名為 的簡單協議,URLSessionProtocol其中包含一個建立資料任務的方法URL。它還定義了URLSessionStub, 符合此協議。它的初始化程式允許您定義資料任務應返回的資料、響應和錯誤。

要設定偽造,請轉到BullsEyeFakeTests.swift並新增一個新測試:

``` func testStartNewRoundUsesRandomValueFromApiRequest () { // 給定 // 1 let stubbedData = "[1]" .data(using: .utf8) let urlString =
"http://www.randomnumberapi.com/api/v1.0/random?min =0&max=100&count=1" 讓url = URL (string: urlString) ! 讓stubbedResponse = HTTPURLResponse ( 網址:網址, 狀態碼:200, http版本:無, headerFields: nil ) 讓urlSessionStub = URLSessionStub ( 資料:存根資料, 響應:存根響應, 錯誤:無) sut.urlSession = urlSessionStub 讓promise =期望(描述:“收到的價值”)

// 什麼時候 sut.startNewRound { // 然後 // 2 XCTAssertEqual ( self .sut.targetValue, 1 ) promise.fulfill() } 等待(為:[承諾],超時:5) } ```

這個測試做了兩件事:

  1. 您設定假資料和響應並建立假會話物件。最後,將假會話作為sut.
  2. 您仍然必須將其編寫為非同步測試,因為存根偽裝成非同步方法。通過與存根的假號碼進行比較,檢查呼叫是否startNewRound(completion:)解析了假資料。targetValue

執行測試。它應該很快就會成功,因為沒有任何真正的網路連線!

偽造模擬物件的更新

之前的測試使用存根來提供來自假物件的輸入。接下來,您將使用一個模擬物件來測試您的程式碼是否正確更新UserDefaults

這個應用程式有兩種遊戲風格。使用者可以:

  1. 移動滑塊以匹配目標值。
  2. 從滑塊位置猜測目標值。

右下角的分段控制元件切換遊戲風格並將其儲存為UserDefaults.

您的下一個測試檢查應用程式是否正確儲存了該gameStyle屬性。

向目標BullsEyeTests新增一個新的測試類並將其命名為BullsEyeMockTestsimport在語句下面新增以下內容:

``` @testable 匯入BullsEye

類 MockUserDefaults : UserDefaults { var gameStyleChanged = 0 覆蓋 函式 集( _value : Int , forKey defaultName : String ) { if defaultName == " gameStyle " {
遊戲風格改變+= 1 } } } ```

MockUserDefaults覆蓋set(_:forKey:)為增量gameStyleChanged。類似的測試通常會設定一個Bool變數,但遞增Int為您提供了更大的靈活性。例如,您的測試可以檢查應用程式是否只調用該方法一次。

接下來,在BullsEyeMockTests中宣告 SUT 和模擬物件:

var sut:檢視控制器! var mockUserDefaults: MockUserDefaults!

替換setUpWithError()tearDownWithError()

``` 覆蓋 func setUpWithError () throws { try super .setUpWithError() sut = UIStoryboard(名稱:“Main”,捆綁:nil) .instantiateInitialViewController()作為? ViewController mockUserDefaults = MockUserDefaults (suiteName: "testing" ) sut.defaults = mockUserDefaults }

覆蓋 func tearDownWithError ()丟擲{ sut = nil mockUserDefaults = nil try super .tearDownWithError() } ```

這將建立 SUT 和模擬物件,並將模擬物件作為 SUT 的屬性注入。

現在,將模板中的兩個預設測試方法替換為:

``` func testGameStyleCanBeChanged () { // 給定 let segmentedControl = UISegmentedControl ()

// 當 XCTAssertEqual ( mockUserDefaults.gameStyleChanged, 0 , "gameStyleChanged 在 sendActions 之前應該為 0" ) 分段控制.addTarget( 蘇, 動作:#selector ( ViewController.chooseGameStyle ( _ :)), 對於:.valueChanged) segmentedControl.sendActions(for: .valueChanged)

// 然後 XCTAssertEqual ( mockUserDefaults.gameStyleChanged, 1、 “gameStyle使用者預設沒有改變”) } ```

when斷言是在測試方法改變分段控制之前gameStyleChanged標誌為0 。因此,如果then斷言也為真,則意味著set(_:forKey:)只調用了一次。

執行測試。它應該成功。

Xcode 中的 UI 測試

UI 測試允許您測試與使用者介面的互動。UI 測試的工作原理是通過查詢查詢應用程式的 UI 物件,合成事件,然後將事件傳送到這些物件。該 API 使您能夠檢查 UI 物件的屬性和狀態,以將它們與預期狀態進行比較。

在測試導航器中,新增一個新的UI 測試目標。檢查要測試的目標BullsEye,然後接受預設名稱BullsEyeUITests

12.png

開啟BullsEyeUITests.swift並在類的頂部新增這個屬性BullsEyeUITests

var應用程式:XCUIApplication!

刪除tearDownWithError()並替換setUpWithError()以下內容:

嘗試 超級.setUpWithError() continueAfterFailure = false app = XCUIApplication () app.launch()

刪除兩個現有測試並新增一個名為testGameStyleSwitch().

func testGameStyleSwitch () { }

在其中開啟一個新行,然後單擊編輯器視窗底部的testGameStyleSwitch()紅色記錄按鈕:

13.png

這將以將您的互動記錄為測試命令的模式在模擬器中開啟應用程式。應用載入後,點選遊戲風格開關的Slide部分和頂部標籤。再次單擊 Xcode Record按鈕以停止錄製。

您現在有以下三行testGameStyleSwitch()

讓app = XCUIApplication () app.buttons[ “幻燈片” ].tap() app.staticTexts[ "儘可能靠近:" ].tap()

記錄器已建立程式碼來測試您在應用程式中測試的相同操作。輕按一下游戲風格的分段控制元件和頂部標籤。您將使用這些作為基礎來建立您自己的 UI 測試。如果您看到任何其他陳述,只需將其刪除。

第一行復制了您在 中建立的屬性setUpWithError(),因此刪除該行。你還不需要點選任何東西,所以也要.tap()在第 2 行和第 3 行的末尾刪除。現在,開啟旁邊的小選單["Slide"]並選擇segmentedControls.buttons["Slide"]

14.png

你應該留下:

app.segmentedControls.buttons[ “幻燈片” ] app.staticTexts[ "儘可能靠近:" ]

點選任何其他物件,讓記錄器幫助您找到可以在測試中訪問的程式碼。現在,用此程式碼替換這些行以建立給定部分:

// 給定 let slideButton = app.segmentedControls.buttons[ "Slide" ] let typeButton = app.segmentedControls.buttons[ "Type" ] let slideLabel = app.staticTexts[ "儘可能接近:" ] let typeLabel = app.staticTexts[ "猜猜滑塊在哪裡:" ]

現在您已經有了分段控制元件中兩個按鈕的名稱和兩個可能的頂部標籤,請在下面新增以下程式碼:

``` // 然後 如果slideButton.isSelected { XCTAssertTrue (slideLabel.exists) XCTAssertFalse (typeLabel.exists)

型別按鈕.tap() XCTAssertTrue (typeLabel.exists) XCTAssertFalse (slideLabel.exists) } else if typeButton.isSelected { XCTAssertTrue (typeLabel.exists) XCTAssertFalse (slideLabel.exists)

滑動按鈕.tap() XCTAssertTrue (slideLabel.exists) XCTAssertFalse (typeLabel.exists) } ```

tap()這將檢查您在分段控制元件中的每個按鈕上是否存在正確的標籤。執行測試——所有斷言都應該成功。

測試效能

來自蘋果的文件

效能測試獲取您想要評估的程式碼塊並執行十次,收集平均執行時間和執行的標準偏差。這些單獨測量的平均值形成了測試執行的值,然後可以將其與基線進行比較以評估成功或失敗。

編寫效能測試很簡單:只需將要測量的程式碼放入measure(). 此外,您可以指定要衡量的多個指標。

將以下測試新增到BullsEyeTests

func testScoreIsComputedPerformance () { 措施( 指標:[ XCTClockMetric (), XCTCPUMetric (), XCTStorageMetric (), XCTMemoryMetric () ] ) { sut.check(猜測:100) } }

該測試測量多個指標:

  • XCTClockMetric測量經過的時間。
  • XCTCPUMetric跟蹤 CPU 活動,包括 CPU 時間、週期和指令數。
  • XCTStorageMetric告訴您測試程式碼寫入儲存的資料量。
  • XCTMemoryMetric跟蹤使用的實體記憶體量。

measure()執行測試,然後單擊出現在尾隨閉包開頭旁邊的圖示以檢視統計資訊。您可以更改 Metric 旁邊的選定指標

15.png

單擊設定基線以設定參考時間。再次執行效能測試並檢視結果——它可能比基線更好或更差。編輯按鈕允許您將基線重置為這個新結果。

基線是按裝置配置儲存的,因此您可以在多個不同的裝置上執行相同的測試。每個都可以根據特定配置的處理器速度、記憶體等保持不同的基線。

每當您對可能影響被測試方法效能的應用程式進行更改時,請再次執行效能測試以檢視它與基線的比較情況。

啟用程式碼覆蓋率

程式碼覆蓋率工具會告訴您測試實際執行的應用程式程式碼,因此您知道應用程式的哪些部分沒有經過測試——至少目前還沒有。

要啟用程式碼覆蓋率,請編輯方案的測試操作並選中選項選項卡下的收集覆蓋率複選框:

16.png

使用Command-U執行所有測試,然後使用Command-9開啟報告導航器。在該列表的頂部專案下選擇Coverage :

17.png

單擊顯示三角形以檢視BullsEyeGame.swift中的函式和閉包列表:

18.png

滾動getRandomNumber(completion:)檢視覆蓋率為 95.0%。

單擊此函式的箭頭按鈕以開啟該函式的原始檔。當您將滑鼠懸停在右側邊欄中的覆蓋註釋上時,程式碼部分會突出顯示綠色或紅色:

19.png

覆蓋註釋顯示測試命中每個程式碼部分的次數。未呼叫的部分以紅色突出顯示。

實現 100% 的覆蓋率?

你應該多努力爭取 100% 的程式碼覆蓋率?只需谷歌“100% 單元測試覆蓋率”,您就會發現支援和反對這一點的一系列論據,以及關於“100% 覆蓋率”定義的爭論。反對它的論點說最後 10%–15% 不值得努力。它的論據說最後 10%–15% 是最重要的,因為它很難測試。谷歌“難以對糟糕的設計進行單元測試”以找到有說服力的論點,即不可測試的程式碼是更深層次設計問題的標誌

本教程的最終專案資料下載地址

連結:https://pan.baidu.com/s/1OyFTBuczz3nLoArvmdSm-g

提取碼:17da

這裡也推薦一些面試相關的內容! * ① BAT等各個大廠iOS面試真題+答案大全