版本記錄
版本號 | 時(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)行測試断序。
本教程將向您展示如何:
- 使用
Xcode
的Test 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()
和示例測試方法定義XCTestCase
的BullsEyeTests
子類纱昧。
您可以通過三種方式運(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")
}
guess
和targetValue
之間的差仍然是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
:
guess
是targetValue ? 5
悲靴,但scoreRound
是105
,而不是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 :)
是否通過將targetValue
與stub
偽造的數(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
中聲明SUT
和mock
對象:
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)建SUT
和mock
對象,并將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 Tested
是BullsEye
,然后接受默認(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)行測試妆丘。
要啟用代碼覆蓋率锄俄,請編輯scheme
的Test
操作,然后選中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)一步研究的資源:
-
WWDC
有幾個(gè)有關(guān)測試主題的視頻慎王。WWDC17
的兩個(gè)不錯(cuò)的選擇是:Engineering for Testability 和Testing Tips & Tricks。 - 下一步是自動(dòng)化:
Continuous Integration
和Continuous Delivery
宏侍。從我們的教程開始赖淤,Continuous Integration With GitHub, Fastlane & Jenkins 和Xcode Server for iOS: Getting Started。
然后谅河,看看Apple的Xcode Server
的Automating the Test Process和xcodebuild
自動(dòng)化測試過程咱旱,以及Wikipedia’s continuous delivery article,該文章借鑒了ThoughtWorks的專業(yè)知識绷耍。
- 如果您已經(jīng)有一個(gè)應(yīng)用程序但尚未編寫測試吐限,則可能要參考Working Effectively with Legacy Code by Michael Feathers,因?yàn)闆]有測試的代碼是舊版代碼褂始!
- 喬恩·里德
(Jon Reid)
的Quality Coding
示例應(yīng)用檔案庫非常適合了解有關(guān)Test-Driven Development的更多信息诸典。
后記
本篇主要講述了關(guān)于
Unit Testing
和UI Testing
,感興趣的給個(gè)贊或者關(guān)注~~~