自動(dòng)化Test使用詳細(xì)解析(六) —— 關(guān)于Unit Testing 和 UI Testing(一)

版本記錄

版本號 時(shí)間
V1.0 2021.05.20 星期四

前言

自動(dòng)化Test可以通過編寫代碼、或者是記錄開發(fā)者的操作過程并代碼化腰池,來實(shí)現(xiàn)自動(dòng)化測試等功能冕末。接下來幾篇我們就說一下該技術(shù)的使用臼予。感興趣的可以看下面幾篇。
1. 自動(dòng)化Test使用詳細(xì)解析(一) —— 基本使用(一)
2. 自動(dòng)化Test使用詳細(xì)解析(二) —— 單元測試和UI Test使用簡單示例(一)
3. 自動(dòng)化Test使用詳細(xì)解析(三) —— 單元測試和UI Test使用簡單示例(二)
4. 自動(dòng)化Test使用詳細(xì)解析(四) —— 單元測試和UI Test(一)
5. 自動(dòng)化Test使用詳細(xì)解析(五) —— 單元測試和UI Test(二)

開始

首先看下主要內(nèi)容:

了解如何將單元測試和UI測試添加到iOS應(yīng)用程序胯甩,以及如何檢查代碼覆蓋率昧廷。內(nèi)容來自翻譯

接著就看下寫作環(huán)境:

Swift 5, iOS 14, Xcode 12

下面就是正文啦偎箫。

iOS單元測試雖然魅力十足木柬,但是由于測試可以防止您的閃亮應(yīng)用程序變成bug纏身的垃圾,因此這很有必要淹办。如果您正在閱讀本教程眉枕,則已經(jīng)知道應(yīng)該為代碼和UI編寫測試,但是可能不知道如何做怜森。

您可能有一個(gè)正在運(yùn)行的應(yīng)用程序速挑,但是您想測試為擴(kuò)展該應(yīng)用程序所做的更改。也許您已經(jīng)編寫了測試塔插,但是不確定它們是否是正確的測試梗摇。或者想许,您已經(jīng)開始開發(fā)新應(yīng)用伶授,并希望隨時(shí)進(jìn)行測試断序。

本教程將向您展示如何:

  • 使用XcodeTest navigator來測試應(yīng)用程序的模型和異步方法
  • 使用stubs and mocks與庫或系統(tǒng)對象的虛假交互
  • 測試用戶界面UI和性能
  • 使用代碼覆蓋率工具

在此過程中,您將掌握測試忍者所使用的一些詞匯糜烹。

打開入門項(xiàng)目违诗,它包括基于BulletEye的項(xiàng)目,該項(xiàng)目基于UIKit Apprentice中的示例應(yīng)用程序疮蹦。這是一個(gè)簡單的運(yùn)氣和運(yùn)氣游戲诸迟。游戲邏輯位于BullsEyeGame類中,您將在本教程中對其進(jìn)行測試愕乎。


Figuring out What to Test

在編寫任何測試之前阵苇,了解基本很重要。您需要測試什么感论?

如果您的目標(biāo)是擴(kuò)展現(xiàn)有應(yīng)用程序绅项,則應(yīng)首先為計(jì)劃更改的任何組件編寫測試。

通常比肄,測試應(yīng)涵蓋:

  • Core functionality:模型類和方法及其與控制器的交互
  • 最常見的UI工作流程
  • 邊界條件
  • Bug修復(fù)

1. Understanding Best Practices for Testing

首字母縮寫詞FIRST描述了有效單元測試的一組簡明標(biāo)準(zhǔn)快耿。這些標(biāo)準(zhǔn)是:

  • Fast - 快速:測試應(yīng)該快速進(jìn)行。
  • Independent/Isolated - 獨(dú)立/隔離:測試不應(yīng)相互共享狀態(tài)芳绩。
  • Repeatable - 可重復(fù):每次運(yùn)行測試時(shí)掀亥,您都應(yīng)獲得相同的結(jié)果。外部數(shù)據(jù)提供者或并發(fā)問題可能會導(dǎo)致間歇性故障妥色。
  • Self-validating - 自我驗(yàn)證:測試應(yīng)完全自動(dòng)化搪花。輸出應(yīng)該是“pass” or “fail”,而不是依賴程序員對日志文件的解釋垛膝。
  • **及時(shí)性:理想情況下鳍侣,您應(yīng)該在編寫要測試的生產(chǎn)代碼之前編寫測試。這就是所謂的測試驅(qū)動(dòng)開發(fā)吼拥。

遵循FIRST原則將使您的測試清晰倚聚,有用,而不會成為您應(yīng)用程序的障礙凿可。


Unit Testing in Xcode

Test navigator提供了最簡單的測試方法惑折。 您將使用它來創(chuàng)建test targets并針對您的應(yīng)用運(yùn)行測試。

1. Creating a Unit Test Target

打開BullsEye項(xiàng)目枯跑,然后按Command-6打開Test navigator惨驶。

單擊左下角的+,然后從菜單中選擇New Unit Test Target…

接受默認(rèn)名稱BullsEyeTests敛助,然后輸入com.raywenderlich作為Organization Identifier粗卜。 當(dāng)test bundle出現(xiàn)在Test navigator中時(shí),通過單擊顯示三角形將其展開纳击,然后單擊BullsEyeTests以在編輯器中將其打開续扔。

默認(rèn)模板導(dǎo)入測試框架XCTest攻臀,并使用setUpWithError()tearDownWithError()和示例測試方法定義XCTestCaseBullsEyeTests子類纱昧。

您可以通過三種方式運(yùn)行測試:

  • 1) Product ? Test or Command-U刨啸。 這兩個(gè)都運(yùn)行所有測試類。
  • 2) 單擊Test navigator中的箭頭按鈕识脆。
  • 3) 單擊裝訂線中的菱形按鈕设联。

您也可以通過在Test navigator或裝訂線中單擊其菱形來運(yùn)行單個(gè)測試方法。

嘗試不同的方式運(yùn)行測試灼捂,以了解所需的時(shí)間和外觀离例。 樣本測試尚無任何功能,因此運(yùn)行速度非匙荻快粘招!

當(dāng)所有測試均成功后,菱形將變?yōu)榫G色并顯示選中標(biāo)記偎球。 單擊testPerformanceExample()末尾的灰色菱形以打開Performance Result

您不需要本教程的testPerformanceExample()testExample(),因此請將其刪除辑甜。

2. Using XCTAssert to Test Models

首先衰絮,您將使用XCTAssert函數(shù)測試BullsEye模型的核心功能:BullsEyeGame是否正確計(jì)算一輪得分?

BullsEyeTests.swift中磷醋,將此行添加到import XCTest下面:

@testable import BullsEye

這使單元測試可以訪問BullsEye中的internal類型和函數(shù)猫牡。

BullsEyeTests的頂部,添加以下屬性:

var sut: BullsEyeGame!

這將為BullsEyeGame創(chuàng)建一個(gè)占位符邓线,它是System Under Test (SUT)或此測試用例類與測試有關(guān)的對象淌友。

接下來,用以下內(nèi)容替換setUpWithError()的內(nèi)容:

try super.setUpWithError()
sut = BullsEyeGame()

這樣會在類級別創(chuàng)建BullsEyeGame骇陈,因此該測試類中的所有測試都可以訪問SUT對象的屬性和方法震庭。

在忘記之前,請?jiān)?code>tearDownWithError()中釋放您的SUT對象你雌。 將其內(nèi)容替換為:

sut = nil
try super.tearDownWithError()

注意:最好的做法是在setUpWithError()中創(chuàng)建SUT器联,然后在tearDownWithError()中釋放它,以確保每次測試都從干凈的開始婿崭。 有關(guān)更多討論拨拓,請查看Jon Reid’s post關(guān)于該主題的帖子。


Writing Your First Test

現(xiàn)在氓栈,您可以開始編寫第一個(gè)測試了渣磷!

將以下代碼添加到BullsEyeTests的末尾,以測試您是否計(jì)算了預(yù)期得分:

func testScoreIsComputedWhenGuessIsHigherThanTarget() {
  // given
  let guess = sut.targetValue + 5

  // when
  sut.check(guess: guess)

  // then
  XCTAssertEqual(sut.scoreRound, 95, "Score computed from guess is wrong")
}

測試方法的名稱始終以test開頭授瘦,然后是對其進(jìn)行測試的描述醋界。

最好將測試格式化為given, when and then的部分:

  • 1) Given:在這里竟宋,您可以設(shè)置所需的任何值。 在此示例中物独,您將創(chuàng)建一個(gè)guess值袜硫,以便您可以指定它與targetValue的差異。
  • 2) When:在本節(jié)中挡篓,您將執(zhí)行要測試的代碼:調(diào)用check(guess :)婉陷。
  • 3) Then:在此部分中,您將通過測試失敗的情況顯示一條消息官研,以確認(rèn)期望的結(jié)果秽澳。 在這種情況下,sut.scoreRound應(yīng)該等于95戏羽,因?yàn)樗?code>100-5担神。

單擊裝訂線或Test navigator中的菱形圖標(biāo),運(yùn)行測試始花。 這將構(gòu)建并運(yùn)行該應(yīng)用程序稿湿,菱形圖標(biāo)將變?yōu)榫G色的選中標(biāo)記! 您還將看到Xcode上方出現(xiàn)一個(gè)短暫的彈出窗口榜轿,它也表示成功器虾,如下所示:

注意:要查看XCTestAssertions的完整列表,請轉(zhuǎn)到Apple’s Assertions Listed by Category浇垦。

1. Debugging a Test

BullsEyeGame特意內(nèi)置了一個(gè)bug炕置,您將立即練習(xí)查找它。 要查看運(yùn)行中的bug男韧,您將創(chuàng)建一個(gè)測試朴摊,該測試將在給定部分的targetValue中減去5,并使其他所有內(nèi)容保持不變此虑。

添加以下測試:

func testScoreIsComputedWhenGuessIsLowerThanTarget() {
  // given
  let guess = sut.targetValue - 5

  // when
  sut.check(guess: guess)

  // then
  XCTAssertEqual(sut.scoreRound, 95, "Score computed from guess is wrong")
}

guesstargetValue之間的差仍然是5甚纲,因此分?jǐn)?shù)仍然應(yīng)該是95

Breakpoint navigator中寡壮,添加Test Failure Breakpoint贩疙。 當(dāng)測試方法發(fā)布故障斷言時(shí),這將停止測試運(yùn)行况既。

運(yùn)行您的測試这溅,它應(yīng)該在XCTAssertEqual行處停止,并顯示測試失敗棒仍。

檢查調(diào)試控制臺中的sut and gues

guesstargetValue ? 5悲靴,但scoreRound105,而不是95莫其!

若要進(jìn)行進(jìn)一步調(diào)查癞尚,請使用正常的調(diào)試過程:在when語句中設(shè)置一個(gè)斷點(diǎn)耸三,并在check(guess :)內(nèi)部的BullsEyeGame.swift中設(shè)置一個(gè)斷點(diǎn),以在此創(chuàng)建difference浇揩。 然后仪壮,再次運(yùn)行測試,并通過let Difference語句檢查應(yīng)用程序中的difference值:

問題在于difference為負(fù)胳徽,因此分?jǐn)?shù)為100-(-5)积锅。要解決此問題,您應(yīng)該使用difference的絕對值养盗。在check(guess :)中缚陷,取消注釋正確的行并刪除不正確的行。

刪除兩個(gè)斷點(diǎn)往核,然后再次運(yùn)行測試以確認(rèn)現(xiàn)在可以成功進(jìn)行箫爷。

2. Using XCTestExpectation to Test Asynchronous Operations

現(xiàn)在,您已經(jīng)了解了如何測試模型和調(diào)試測試失敗聂儒,現(xiàn)在該著手測試異步代碼了虎锚。

BullsEyeGame使用URLSession獲得一個(gè)隨機(jī)數(shù)作為下一個(gè)游戲的目標(biāo)。 URLSession方法是異步的:它們會立即返回衩婚,但要等到稍后才結(jié)束運(yùn)行翁都。要測試異步方法,請使用XCTestExpectation使測試等待異步操作完成谅猾。

異步測試通常很慢,因此應(yīng)將它們與更快的單元測試分開鳍悠。

創(chuàng)建一個(gè)名為BullsEyeSlowTests的新單元測試目標(biāo)税娜。打開全新的測試類BullsEyeSlowTests,然后在現(xiàn)有import語句下方導(dǎo)入BullsEye應(yīng)用模塊:

@testable import BullsEye

此類中的所有測試都使用默認(rèn)的URLSession發(fā)送請求藏研,因此聲明sut敬矩,在setUpWithError()中創(chuàng)建它蠢挡,然后在tearDownWithError()中釋放它弧岳。 為此,將BullsEyeSlowTests的內(nèi)容替換為:

var sut: URLSession!

override func setUpWithError() throws {
  try super.setUpWithError()
  sut = URLSession(configuration: .default)
}

override func tearDownWithError() throws {
  sut = nil
  try super.tearDownWithError()
}

接下來业踏,添加此異步測試:

// Asynchronous test: success fast, failure slow
func testValidApiCallGetsHTTPStatusCode200() throws {
  // given
  let urlString = 
    "http://www.randomnumberapi.com/api/v1.0/random?min=0&max=100&count=1"
  let url = URL(string: urlString)!
  // 1
  let promise = expectation(description: "Status code: 200")

  // when
  let dataTask = sut.dataTask(with: url) { _, response, error in
    // then
    if let error = error {
      XCTFail("Error: \(error.localizedDescription)")
      return
    } else if let statusCode = (response as? HTTPURLResponse)?.statusCode {
      if statusCode == 200 {
        // 2
        promise.fulfill()
      } else {
        XCTFail("Status code: \(statusCode)")
      }
    }
  }
  dataTask.resume()
  // 3
  wait(for: [promise], timeout: 5)
}

此測試檢查發(fā)送有效請求是否返回200狀態(tài)碼禽炬。 大多數(shù)代碼與您在應(yīng)用程序中編寫的代碼相同,但有以下幾行:

  • 1) Expectation(description :):返回存儲在Promise中的XCTestExpectation勤家。 description描述了您期望發(fā)生的事情腹尖。
  • 2) promise.fulfill():在異步方法的完成處理程序的成功條件閉包中調(diào)用此函數(shù),以標(biāo)記已達(dá)到期望伐脖。
  • 3) wait(for:timeout :):保持測試運(yùn)行热幔,直到滿足所有期望或timeout間隔結(jié)束(以先發(fā)生者為準(zhǔn))乐设。

運(yùn)行測試。 如果您已連接到互聯(lián)網(wǎng)绎巨,則在將應(yīng)用程序加載到模擬器中后近尚,測試大約需要一秒鐘才能成功。

3. Failing Fast

失敗很痛苦场勤,但這并不一定要長久戈锻。

要體驗(yàn)失敗,只需將testValidApiCallGetsHTTPStatusCode200()中的URL更改為無效的URL

let url = URL(string: "http://www.randomnumberapi.com/test")!

運(yùn)行測試却嗡。 它失敗舶沛,但是需要整個(gè)超時(shí)間隔! 這是因?yàn)槟僭O(shè)請求將始終成功窗价,因此您將其稱為promise.fulfill()如庭。 由于請求失敗,因此僅在超時(shí)到期時(shí)才完成撼港。

您可以改善這一點(diǎn)坪它,并通過更改假設(shè)使測試更快地失敗。 不必等待請求成功帝牡,只需等待異步方法的完成處理程序被調(diào)用即可往毡。 一旦應(yīng)用程序從服務(wù)器接收到滿足預(yù)期的響應(yīng)(“OK”“error”),就會發(fā)生這種情況靶溜。 然后开瞭,您的測試可以檢查請求是否成功。

要查看其工作原理罩息,請創(chuàng)建一個(gè)新測試嗤详。

但首先,通過撤消對url所做的更改來修復(fù)以前的測試瓷炮。

然后葱色,將以下測試添加到您的類:

func testApiCallCompletes() throws {
  // given
  let urlString = "http://www.randomnumberapi.com/test"
  let url = URL(string: urlString)!
  let promise = expectation(description: "Completion handler invoked")
  var statusCode: Int?
  var responseError: Error?

  // when
  let dataTask = sut.dataTask(with: url) { _, response, error in
    statusCode = (response as? HTTPURLResponse)?.statusCode
    responseError = error
    promise.fulfill()
  }
  dataTask.resume()
  wait(for: [promise], timeout: 5)

  // then
  XCTAssertNil(responseError)
  XCTAssertEqual(statusCode, 200)
}

關(guān)鍵區(qū)別在于,只需輸入完成處理程序即可滿足期望娘香,而這僅需一秒鐘即可完成苍狰。 如果請求失敗,then斷言失敗烘绽。

運(yùn)行測試淋昭。 現(xiàn)在需要大約一秒鐘才能失敗。 它失敗是因?yàn)檎埱笫【饕Γ皇且驗(yàn)闇y試運(yùn)行超出了timeout响牛。

修復(fù)url,然后再次運(yùn)行測試以確認(rèn)它現(xiàn)在可以成功。

4. Failing Conditionally

在某些情況下呀打,執(zhí)行測試沒有多大意義矢赁。 例如,當(dāng)testValidApiCallGetsHTTPStatusCode200()在沒有網(wǎng)絡(luò)連接的情況下運(yùn)行時(shí)會發(fā)生什么贬丛? 當(dāng)然撩银,它不應(yīng)該通過,因?yàn)樗粫盏?code>200狀態(tài)代碼豺憔。 但是它也不應(yīng)該失敗额获,因?yàn)樗鼪]有測試任何東西。

幸運(yùn)的是恭应,Apple推出了XCTSkip抄邀,以在前提條件失敗時(shí)跳過測試。 在sut聲明下面添加以下行:

let networkMonitor = NetworkMonitor.shared

NetworkMonitor包裝NWPathMonitor昼榛,從而提供了一種方便的方法來檢查網(wǎng)絡(luò)連接境肾。

testValidApiCallGetsHTTPStatusCode200()中,在測試開始時(shí)添加XCTSkipUnless

try XCTSkipUnless(
  networkMonitor.isReachable, 
  "Network connectivity needed for this test.")

當(dāng)沒有網(wǎng)絡(luò)可訪問時(shí)胆屿,XCTSkipUnless(_:_ :)跳過測試奥喻。 通過禁用網(wǎng)絡(luò)連接并運(yùn)行測試來進(jìn)行檢查。 您會在測試旁邊的裝訂線中看到一個(gè)新圖標(biāo)非迹,表明該測試未通過或未通過环鲤。

再次啟用您的網(wǎng)絡(luò)連接,然后重新運(yùn)行測試以確保它在正常情況下仍然成功憎兽。將相同的代碼添加到testApiCallCompletes()的開頭冷离。


Faking Objects and Interactions

異步測試使您有信心代碼可以為異步API生成正確的輸入。您可能還需要測試從URLSession接收輸入時(shí)代碼是否正常工作纯命,或者是否正確更新UserDefaults數(shù)據(jù)庫或iCloud容器酒朵。

大多數(shù)應(yīng)用與系統(tǒng)或庫對象(您無法控制的對象)進(jìn)行交互。與這些對象進(jìn)行交互的測試可能是緩慢且不可重復(fù)的扎附,這違反了FIRST的兩個(gè)原則。相反结耀,您可以通過從stubs獲取輸入或通過更新mock對象來偽造交互留夜。

當(dāng)您的代碼依賴于系統(tǒng)或庫對象時(shí),請進(jìn)行偽造图甜。為此碍粥,可以創(chuàng)建一個(gè)假對象來扮演該角色,并將該假對象注入代碼中黑毅。喬恩·里德(Jon Reid)的Dependency Injection描述了幾種方法嚼摩。

1. Faking Input From Stub

現(xiàn)在,檢查應(yīng)用程序的getRandomNumber(completion :)是否正確解析了會話下載的數(shù)據(jù)。您將使用存根數(shù)據(jù)偽造BullsEyeGame的會話枕面。

轉(zhuǎn)到Test navigator愿卒,單擊+,然后選擇New Unit Test Class…潮秘。將其命名為BullsEyeFakeTests琼开,將其保存在BullsEyeTests目錄中,然后將目標(biāo)設(shè)置為BullsEyeTests枕荞。

import語句下方導(dǎo)入BullsEye應(yīng)用模塊:

@testable import BullsEye

現(xiàn)在柜候,用以下內(nèi)容替換BullsEyeFakeTests的內(nèi)容:

var sut: BullsEyeGame!

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

override func tearDownWithError() throws {
  sut = nil
  try super.tearDownWithError()
}

這將聲明SUT,即BullsEyeGame躏精,在setUpWithError()中創(chuàng)建它渣刷,并在tearDownWithError()中釋放它。

BullsEye項(xiàng)目包含支持文件URLSessionStub.swift矗烛。 這定義了一個(gè)名為URLSessionProtocol的簡單協(xié)議辅柴,并帶有使用URL創(chuàng)建數(shù)據(jù)任務(wù)的方法。 它還定義了符合此協(xié)議的URLSessionStub高诺。 它的初始化程序使您可以定義數(shù)據(jù)任務(wù)應(yīng)返回的數(shù)據(jù)碌识,響應(yīng)和錯(cuò)誤。

要設(shè)置偽造虱而,請轉(zhuǎn)到BullsEyeFakeTests.swift并添加一個(gè)新測試:

func testStartNewRoundUsesRandomValueFromApiRequest() {
  // given
  // 1
  let stubbedData = "[1]".data(using: .utf8)
  let urlString = 
    "http://www.randomnumberapi.com/api/v1.0/random?min=0&max=100&count=1"
  let url = URL(string: urlString)!
  let stubbedResponse = HTTPURLResponse(
    url: url, 
    statusCode: 200, 
    httpVersion: nil, 
    headerFields: nil)
  let urlSessionStub = URLSessionStub(
    data: stubbedData,
    response: stubbedResponse, 
    error: nil)
  sut.urlSession = urlSessionStub
  let promise = expectation(description: "Value Received")

  // when
  sut.startNewRound {
    // then
    // 2
    XCTAssertEqual(self.sut.targetValue, 1)
    promise.fulfill()
  }
  wait(for: [promise], timeout: 5)
}

該測試執(zhí)行兩件事:

  • 1) 您設(shè)置了偽造的數(shù)據(jù)和響應(yīng)筏餐,并創(chuàng)建了偽造的會話對象。最后牡拇,將假會話作為sut的屬性注入到應(yīng)用程序中魁瞪。
  • 2) 您仍然必須將其編寫為異步測試,因?yàn)?code>stub假裝是異步方法惠呼。檢查調(diào)用startNewRound(completion :)是否通過將targetValuestub偽造的數(shù)字進(jìn)行比較來解析偽造的數(shù)據(jù)导俘。

運(yùn)行測試。它應(yīng)該很快就能成功剔蹋,因?yàn)闆]有任何實(shí)際的網(wǎng)絡(luò)連接旅薄!

2. Faking an Update to Mock Object

先前的測試使用stub提供來自偽造對象的輸入。接下來泣崩,您將使用mock object來測試您的代碼是否正確更新了UserDefaults少梁。

這個(gè)程序有兩種游戲風(fēng)格。用戶可以:

  • 1) 移動(dòng)滑塊以匹配目標(biāo)值矫付。
  • 2) 從滑塊位置猜測目標(biāo)值凯沪。

右下角的分段控件可切換游戲樣式并將其保存在UserDefaults中。

您的下一個(gè)測試將檢查應(yīng)用程序是否正確保存了gameStyle屬性买优。

target BullsEyeTests添加一個(gè)新的測試類妨马,并將其命名為BullsEyeMockTests挺举。在import語句下面添加以下內(nèi)容:

@testable import BullsEye

class MockUserDefaults: UserDefaults {
  var gameStyleChanged = 0
  override func set(_ value: Int, forKey defaultName: String) {
    if defaultName == "gameStyle" {
      gameStyleChanged += 1
    }
  }
}

MockUserDefaults重寫set(_:forKey :)以增加gameStyleChanged。 相似的測試通常會設(shè)置一個(gè)Bool變量烘跺,但是遞增Int可以為您提供更大的靈活性湘纵。 例如,您的測試可以檢查應(yīng)用程序僅調(diào)用一次該方法液荸。

接下來瞻佛,在BullsEyeMockTests中聲明SUTmock對象:

var sut: ViewController!
var mockUserDefaults: MockUserDefaults!

setUpWithError()tearDownWithError()替換為:

override func setUpWithError() throws {
  try super.setUpWithError()
  sut = UIStoryboard(name: "Main", bundle: nil)
    .instantiateInitialViewController() as? ViewController
  mockUserDefaults = MockUserDefaults(suiteName: "testing")
  sut.defaults = mockUserDefaults
}

override func tearDownWithError() throws {
  sut = nil
  mockUserDefaults = nil
  try super.tearDownWithError()
}

這將創(chuàng)建SUTmock對象,并將mock對象作為SUT的屬性注入娇钱。

現(xiàn)在伤柄,將模板中的兩個(gè)默認(rèn)測試方法替換為:

func testGameStyleCanBeChanged() {
  // given
  let segmentedControl = UISegmentedControl()

  // when
  XCTAssertEqual(
    mockUserDefaults.gameStyleChanged, 
    0, 
    "gameStyleChanged should be 0 before sendActions")
  segmentedControl.addTarget(
    sut,
    action: #selector(ViewController.chooseGameStyle(_:)),
    for: .valueChanged)
  segmentedControl.sendActions(for: .valueChanged)

  // then
  XCTAssertEqual(
    mockUserDefaults.gameStyleChanged, 
    1, 
    "gameStyle user default wasn't changed")
}

when斷言是在測試方法更改分段控件之前gameStyleChanged標(biāo)志為0。 因此文搂,如果then斷言也成立适刀,則意味著set(_:forKey :)恰好被調(diào)用了一次。

運(yùn)行測試煤蹭。 它應(yīng)該成功笔喉。


UI Testing in Xcode

UI測試使您可以測試與用戶界面的交互。 用戶界面測試的工作原理是通過查詢查找應(yīng)用程序的用戶界面對象硝皂,綜合事件常挚,然后將事件發(fā)送到這些對象。 使用該API稽物,您可以檢查UI對象的屬性和狀態(tài)奄毡,以將其與預(yù)期狀態(tài)進(jìn)行比較。

Test navigator中贝或,添加一個(gè)新的UI Test Target吼过。 檢查Target to be TestedBullsEye,然后接受默認(rèn)名稱BullsEyeUITests咪奖。

打開BullsEyeUITests.swift并將此屬性添加到BullsEyeUITests類的頂部:

var app: XCUIApplication!

刪除tearDownWithError()并將setUpWithError()的內(nèi)容替換為以下內(nèi)容:

try super.setUpWithError()
continueAfterFailure = false
app = XCUIApplication()
app.launch()

刪除兩個(gè)現(xiàn)有的測試盗忱,并添加一個(gè)名為testGameStyleSwitch()的新測試。

func testGameStyleSwitch() {    
}

testGameStyleSwitch()中打開新行羊赵,然后單擊編輯器窗口底部的紅色Record按鈕:

這會以將您的互動(dòng)記錄為測試命令的模式在模擬器中打開該應(yīng)用趟佃。 應(yīng)用加載后,點(diǎn)擊游戲樣式開關(guān)的Slide部分和top label昧捷。 再次單擊Xcode的Record按鈕以停止記錄揖闸。

現(xiàn)在,您在testGameStyleSwitch()中具有以下三行:

let app = XCUIApplication()
app.buttons["Slide"].tap()
app.staticTexts["Get as close as you can to: "].tap()

記錄器已創(chuàng)建代碼以測試您在應(yīng)用程序中測試的相同操作料身。 向游戲樣式分段控件和頂部標(biāo)簽發(fā)送點(diǎn)擊。 您將以此為基礎(chǔ)來創(chuàng)建自己的UI測試衩茸。 如果您看到其他任何語句芹血,則將其刪除。

第一行與您在setUpWithError()中創(chuàng)建的屬性重復(fù),因此請刪除該行幔烛。 您無需點(diǎn)擊任何東西啃擦,因此也請刪除第2行和第3行結(jié)尾的.tap()。現(xiàn)在饿悬,打開[“ Slide”]旁邊的小菜單令蛉,然后選擇segmentedControls.buttons [“ Slide”]

您應(yīng)該得到:

app.segmentedControls.buttons["Slide"]
app.staticTexts["Get as close as you can to: "]

點(diǎn)擊其他任何對象狡恬,讓記錄器幫助您找到可以在測試中訪問的代碼珠叔。 現(xiàn)在,用以下代碼替換這些行以創(chuàng)建給定的部分:

// given
let slideButton = app.segmentedControls.buttons["Slide"]
let typeButton = app.segmentedControls.buttons["Type"]
let slideLabel = app.staticTexts["Get as close as you can to: "]
let typeLabel = app.staticTexts["Guess where the slider is: "]

現(xiàn)在弟劲,您已經(jīng)有了分段控件中兩個(gè)按鈕的名稱以及兩個(gè)可能的頂部標(biāo)簽祷安,在下面添加以下代碼:

// then
if slideButton.isSelected {
  XCTAssertTrue(slideLabel.exists)
  XCTAssertFalse(typeLabel.exists)

  typeButton.tap()
  XCTAssertTrue(typeLabel.exists)
  XCTAssertFalse(slideLabel.exists)
} else if typeButton.isSelected {
  XCTAssertTrue(typeLabel.exists)
  XCTAssertFalse(slideLabel.exists)

  slideButton.tap()
  XCTAssertTrue(slideLabel.exists)
  XCTAssertFalse(typeLabel.exists)
}

當(dāng)您在分段控件中的每個(gè)按鈕上tap()時(shí),這將檢查是否存在正確的label兔乞。 運(yùn)行測試 —— 所有斷言都應(yīng)成功汇鞭。


Testing Performance

根據(jù)Apple’s documentation

A performance test takes a block of code that you want to evaluate and runs it ten times, collecting the average execution time and the standard deviation for the runs. The averaging of these individual measurements form a value for the test run that can then be compared against a baseline to evaluate success or failure.

編寫性能測試很簡單:只需將要測量的代碼放在measure()的結(jié)尾處。 此外庸追,您可以指定多個(gè)指標(biāo)進(jìn)行衡量霍骄。

將以下測試添加到BullsEyeTests

func testScoreIsComputedPerformance() {
  measure(
    metrics: [
      XCTClockMetric(), 
      XCTCPUMetric(),
      XCTStorageMetric(), 
      XCTMemoryMetric()
    ]
  ) {
    sut.check(guess: 100)
  }
}

此測試測量多個(gè)指標(biāo):

  • XCTClockMetric度量經(jīng)過的時(shí)間。
  • XCTCPUMetric跟蹤CPU活動(dòng)淡溯,包括CPU時(shí)間读整,周期和指令數(shù)。
  • XCTStorageMetric告訴您測試代碼將多少數(shù)據(jù)寫入存儲血筑。
  • XCTMemoryMetric跟蹤已使用的物理內(nèi)存量绘沉。

運(yùn)行測試,然后單擊measure()尾隨閉包開頭旁邊顯示的圖標(biāo)以查看統(tǒng)計(jì)信息豺总。 您可以在Metric旁邊更改所選指標(biāo)车伞。

單擊Set Baseline以設(shè)置參考時(shí)間。 再次運(yùn)行性能測試并查看結(jié)果 —— 它可能比基準(zhǔn)更好或更差喻喳。 使用Edit按鈕可以將基準(zhǔn)重置為該新結(jié)果另玖。

基準(zhǔn)是按設(shè)備配置存儲的,因此您可以在多個(gè)不同的設(shè)備上執(zhí)行相同的測試表伦。 每個(gè)都可以維持不同的基準(zhǔn)谦去,具體取決于特定配置的處理器速度,內(nèi)存等蹦哼。

每當(dāng)您對應(yīng)用程序進(jìn)行更改而可能影響所測試方法的性能時(shí)鳄哭,請?jiān)俅芜\(yùn)行性能測試以查看其與基準(zhǔn)的比較情況。


Enabling Code Coverage

代碼覆蓋率工具會告訴您測試實(shí)際在運(yùn)行哪些應(yīng)用程序代碼纲熏,因此您知道該應(yīng)用程序的哪些部分尚未進(jìn)行測試 —— 至少現(xiàn)在尚未進(jìn)行測試妆丘。

要啟用代碼覆蓋率锄俄,請編輯schemeTest操作,然后選中Options標(biāo)簽下的Gather coverage for復(fù)選框:

使用Command-U運(yùn)行所有測試勺拣,然后使用Command-9打開Report navigator奶赠。 選擇該列表頂部項(xiàng)目下的Coverage

單擊顯示三角形以查看BullsEyeGame.swift中的函數(shù)和閉包列表:

滾動(dòng)到getRandomNumber(completion :)以查看覆蓋率為95.0%

單擊此函數(shù)的箭頭按鈕以打開該函數(shù)的源文件药有。 當(dāng)您將鼠標(biāo)懸停在右側(cè)欄中的coverage注釋上時(shí)毅戈,代碼部分將突出顯示綠色或紅色:

覆蓋率注釋顯示測試命中每個(gè)代碼段的次數(shù)。 未調(diào)用的部分以紅色突出顯示愤惰。

1. Achieving 100% Coverage?

您應(yīng)該努力爭取100%的代碼覆蓋率嗎苇经? 只是Google“100% unit test coverage”,您會發(fā)現(xiàn)一系列支持和反對的論點(diǎn)羊苟,以及關(guān)于“100%覆蓋率”這一定義的爭論塑陵。 反對的說法是,最后10% - 15%的努力是不值得的蜡励。 關(guān)于它的說法令花,最后10%– 15%是最重要的,因?yàn)樗茈y測試凉倚。 谷歌查找“hard to unit test bad design”兼都,以找到令人信服的論點(diǎn),即untestable code is a sign of deeper design problems稽寒。

您現(xiàn)在可以使用一些出色的工具來為項(xiàng)目編寫測試扮碧。我希望這個(gè)iOS Unit Testing and UI Testing教程能夠給您信心,可以測試所有東西杏糙!

以下是一些需要進(jìn)一步研究的資源:

然后谅河,看看Apple的Xcode ServerAutomating the Test Processxcodebuild自動(dòng)化測試過程咱旱,以及Wikipedia’s continuous delivery article,該文章借鑒了ThoughtWorks的專業(yè)知識绷耍。

后記

本篇主要講述了關(guān)于Unit TestingUI Testing,感興趣的給個(gè)贊或者關(guān)注~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末崎苗,一起剝皮案震驚了整個(gè)濱河市狐粱,隨后出現(xiàn)的幾起案子赘阀,更是在濱河造成了極大的恐慌,老刑警劉巖脑奠,帶你破解...
    沈念sama閱讀 222,378評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異幅慌,居然都是意外死亡宋欺,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,970評論 3 399
  • 文/潘曉璐 我一進(jìn)店門胰伍,熙熙樓的掌柜王于貴愁眉苦臉地迎上來齿诞,“玉大人,你說我怎么就攤上這事骂租〉昏荆” “怎么了?”我有些...
    開封第一講書人閱讀 168,983評論 0 362
  • 文/不壞的土叔 我叫張陵渗饮,是天一觀的道長但汞。 經(jīng)常有香客問我,道長互站,這世上最難降的妖魔是什么私蕾? 我笑而不...
    開封第一講書人閱讀 59,938評論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮胡桃,結(jié)果婚禮上踩叭,老公的妹妹穿的比我還像新娘。我一直安慰自己翠胰,他們只是感情好容贝,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,955評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著之景,像睡著了一般斤富。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上闺兢,一...
    開封第一講書人閱讀 52,549評論 1 312
  • 那天茂缚,我揣著相機(jī)與錄音,去河邊找鬼屋谭。 笑死脚囊,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的桐磁。 我是一名探鬼主播悔耘,決...
    沈念sama閱讀 41,063評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼我擂!你這毒婦竟也來了衬以?” 一聲冷哼從身側(cè)響起缓艳,我...
    開封第一講書人閱讀 39,991評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎看峻,沒想到半個(gè)月后阶淘,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,522評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡互妓,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,604評論 3 342
  • 正文 我和宋清朗相戀三年溪窒,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片冯勉。...
    茶點(diǎn)故事閱讀 40,742評論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡澈蚌,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出灼狰,到底是詐尸還是另有隱情宛瞄,我是刑警寧澤,帶...
    沈念sama閱讀 36,413評論 5 351
  • 正文 年R本政府宣布交胚,位于F島的核電站份汗,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏承绸。R本人自食惡果不足惜裸影,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,094評論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望军熏。 院中可真熱鬧轩猩,春花似錦、人聲如沸荡澎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,572評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽摩幔。三九已至彤委,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間或衡,已是汗流浹背焦影。 一陣腳步聲響...
    開封第一講書人閱讀 33,671評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留封断,地道東北人斯辰。 一個(gè)月前我還...
    沈念sama閱讀 49,159評論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像坡疼,于是被迫代替她去往敵國和親彬呻。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,747評論 2 361

推薦閱讀更多精彩內(nèi)容