<p> 原文地址:https://www.raywenderlich.com/150073/ios-unit-testing-and-ui-testing-tutorial (March 13, 2017) </p>
<p>雖然寫測試程序不是一件容易令人著迷的事情,但它卻是非常必要的。因為測試可以避免你炫酷的應用成為一個錯誤百出的垃圾。如果你正在閱讀本教程,你應該已經意識到需要編寫測試程序來測試你的代碼和界面了,但你可能還不知道如何在Xcode中進行測試。</p>
<p>也許你已經有了一個可以“工作”的應用程序,但還沒有為它建立測試程序,而且你希望在擴展你的應用時測試可以覆蓋所有的改動。也許你已經寫了一些測試程序,但不確定它們是否正確。又或者你正在寫一個應用,希望能能夠同步進行測試。</p>
<p>這個教程展示了如何使用Xcode中的測試導航來測試你的應用模型和異步方法;如何使用存根(stub)和模擬對象來和庫或者系統對象進行互動;如何測試應用的用戶界面和性能,以及如何使用代碼覆蓋工具。在這個過程中你會遇到一些測試高手的常用詞匯。在本教程的結尾,你將使用aplomb工具來將依賴注入到你的被測試系統中去!</p>
<p>測試,測試…</p>
什么是測試?
<p>在開始寫任何測試程序之前,先考慮一個最基礎的問題:你要測什么?如果你的目標是擴展現有的應用程序,你應該首先為你打算修改的組件編寫測試。</p>
<p>具體來說,測試應該覆蓋以下內容:</p><p><li>核心功能:模型類和方法,以及它們與控制器的交互
<li>最常用到的用戶操作流程
<li>邊界條件
<li>要解的bug</p>
FIRST: 測試的最佳實踐
<p>縮寫FIRST描述了一套簡潔有效的單元測試標準。這些標準是:</p><li>快(F):測試應該執行的盡可能快,這樣人們就不會介意運行它們。
<li>獨立/隔離(I):測試應該相互獨立,有各自的建立(setup)和拆卸(teardown)過程。
<li>重復性(R):每次運行測試都應獲得相同的結果。外部數據和并發問題會可能導致間歇性故障。
<li>自我驗證(S):測試應該是全自動的;輸出的結果要么是“通過”,要么是“失敗”,而不能是輸出一個只有程序員看得懂的日志文件。
<li>及時(T):理想情況下,應該先寫測試代碼再寫對應的生產代碼。
<p>遵循FIRST原則,將使你的測試清晰明了、并且真的可以對開發有幫助,而不是成為應用開發的阻礙。</p>
讓我們開始吧
<p>下載,解壓,打開并查看兩個準備好了的起始項目 BullsEye 和 halftunes。</p>
<p>BullsEye 是基于“iOS學徒”教學中的一個示例應用;我已經將其中的游戲邏輯提取到bullseyegame類中并添加了一種新的玩法。在右下角的分段控件可以讓用戶選擇玩法:要么是移動滑塊來盡可能地接近目標值;或者是通過猜測滑塊的位置來得分。用戶當前的玩法選擇將被作為默認值存儲起來。</p>
<p>Halftuness是來自 NSURLSession教程中的示例應用程序,代碼已經被更新到了Swift 3。用戶可以通過iTunes API查詢歌曲,然后下載和播放歌曲的試聽片段。(譯注:這個項目對新手可能會復雜些,所以我寫了個分析。)</p>
<p>讓我們開始測試吧!</p>
Xcode中的單元測試
創建一個單元測試的目標
Xcode中的測試導航欄提供了非常簡便的進行測試的方法;你將使用它來創建測試目標和為你的應用執行測試程序。
打開BullsEye工程,按?+5切換到測試導航欄。
單擊左下角的“+”按鈕,然后從菜單選擇 ”新的單元測試目標…:
接受默認名稱BullsEyeTests。當測試目標在測試導航欄中出現后,單擊它在編輯器中打開。如果BullsEyeTests沒有自動出現,試試切換到另一個導航欄,再切回。
<p>自動生成的測試類模板會導入<code>XCTest</code>并定義一個XCTestCase的子類<code>BullsEyeTests</code>,以及setup(),teardown()
和示例測試方法。有三種方法可以運行測試類:
</p><ol>
<li>從菜單上選擇Product->Test 或者按 ?+U。 這會執行所有的測試類。</li>
<li>點擊測試導航欄中的箭頭按鈕。</li>
<li>點擊分隔欄上的菱形標志。</li>
</ol>
<p>您也可以點擊每個測試方法所對應的菱形標志來執行該方法,無論是在測試導航欄還是在分隔欄上點都可以。</p>
<p>嘗試不同的方法來執行測試,感受一下所需的時間,還有它們看起來像什么。因為現在測試程序還沒有做任何事,所以執行會很快!</p>
<p>當所有的測試都通過后,菱形標志將變綠,并顯示一個對號。點擊在testPerformanceExample()
方法結尾處的灰色菱形按鈕打開性能結果展示:</p>
<code>testPerformanceExample()</code>這個方法用不到,可以刪除。</p>
使用<code>XCTAssert</code>測試模型
<p>首先,你將使用XCTAssert
測試BullEye的一個核心功能:BullsEyeGame對象在每一輪中計算出的分數是否都正確?打開BullsEyeTests.swift,在import
語句的下方加入這一行:</p>
<p><pre><code>@testable import BullsEye</p></code></pre><p>這使得測試類可以訪問BullsEye中的類和方法。在BullsEyeTests類的頂部,添加屬性:</p>
<p><pre><code>var gameUnderTest: BullsEyeGame!</p></code></pre><p>在setup()
中創建一個新的BullsEyeGame對象,放在對super
的調用的后面:</p>
<p><pre><code>gameUnderTest = BullsEyeGame()</code>
<code>gameUnderTest.startNewGame()</code></pre><p>這將在類中創建一個SUT(被測試系統)對象,所以在這個測試類中的所有測試方法都可以訪問該SUT對象的屬性和方法。</p>
在這里,你還可以調用游戲的startNewGame
方法,創建一個目標值。你的許多測試都將使用該目標值來檢驗游戲是否正確地計算了得分。
<p>在你忘記之前,在<code>tearDown()</code>中釋放該SUT對象,將下面的代碼放在對super
的調用之前:</p>
<pre>gameUnderTest=nil
</pre>
<pre>注:在setup()
中建立SUT并在tearDown()
中釋放它是一個最佳實踐。這可以確保每一個測試在一個干凈的狀態中開始。喬恩瑞德寫的相關文章中有更多的討論。</pre>
現在你已經準備好寫你的第一個測試方法了!使用下面的代碼替換testExample()
<pre>// XCTAssert to test model func testScoreIsComputed(){ // 1. given let guess = gameUnderTest.targetValue + 5 // 2. when _ = gameUnderTest.check(guess:guess) // 3. then XCTAssertEqual(gameUnderTest.scoreRound,95,"Score computed from guess is wrong") }
</pre>
測試方法的名字總是以test開始,緊跟著的是對該測試方法的描述。
以假設(given), 當(when) 和然后(then)三段的方式來組織一個測試方法是一個很好的做法:
在given部分,設置任何測試所需要的值:在這個例子中創建了一個猜測值,并指定它和目標值的差。
在when部分,執行被測試代碼:執行gameUndertest.check(_:)
在then部分,使用斷言來判斷結果(在這個例子中,gameUnderTest.scoreRound
的值是 100-5=95)。如果測試失敗則會打印出一個消息。
通過在分隔欄或測試導航器中單擊菱形標志來執行測試。應用程序將會被構建和執行,然后菱形標志會變成綠色的對號!
注:要想查看完整的XCTestAssertions
列表,按下 ? 并點擊XCTAssertEqual
來打開 XCTestAssertions.h,或去查看蘋果的官方文檔。
注:測試的Given-When-Then結構起源于行為驅動開發(BDD)。這是一種對客戶友好,術語簡單的命名方式。其它的命名系統還有Arrange-Act-Assert (組織-行動-斷言)和Assemble-Activate-Assert(組裝-激活-斷言)。</p>
對測試進行調試
我在BullsEyeGame中故意放置了一個bug,現在你去把它找出來。要發現這個bug,將testScoreIsComputed
方法重命名為testScoreIsComputedWhenGuessGTTarget
,然后復制、粘貼建立一個新方法testScoreIsComputedWhenGuessLTTarget
。
在這個測試方法中,在given段,將.targetValue+5
改為-5
。其它部分不變:
<pre>func testScoreIsComputedWhenGuessLTTarget() { // 1. given let guess = gameUnderTest.targetValue - 5 // 2. when _ = gameUnderTest.check(guess: guess) // 3. then XCTAssertEqual(gameUnderTest.scoreRound, 95, "Score computed from guess is wrong") }
</pre>
guess
和targetValue
之間的差仍然是5,所以得分應該還是95。
在斷點導航欄中,添加一個測試失敗斷點;當測試方法發出失敗斷言時,測試將停止執行。
<p>執行測試:它應該停在XCTAssertEqual
處,并伴隨一個測試失敗的提示。在調試控制臺檢查gameUnderTest
和guess
變量的值:</p>
guess
等于 targetValue - 5
但得分(scoreRound)是105,不是95! (譯注:targetValue是隨機產生的,因而你看到的值很可能會和圖中的不同)
<p>為了進一步調查,我們采取通常的調試過程:在when陳述與BullsEyeGame.swift中導致了差異的check(_:)
處分別設置一個斷點。然后再次執行行測試,斷點生效后單步跳過賦值行來檢查應用中difference
變量的值:</p>
我們發現,問題出在difference
是負值,所以得分是 100-(-5) ;修復方法是使用絕對值。在check(_:)
中,打開正確行,刪除錯誤行。
<p>移除這兩個斷點并再次執行測試以確認它現在是成功的了。</p>
使用 XCTestExpectation 測試異步操作
<p>現在,你已經學會了如何測試模型和調試測試失敗的情況。下面讓我們繼續使用XCTestExpectation來測試網絡操作。</p><p>
打開HalfTunes工程:它利用URLSession
來調用iTunes API查詢和下載歌曲的試聽片段。假設你想修改它,改用AlamoFire來進行網絡操作。要查看改變是否會引入任何問題,您應該為網絡操作編寫測試方法并在該換為AlamoFire的前后運行它們。</p><p>
URLSession
方法是異步調用:它們會立刻返回,但真正結束運行還要等上一段時間。為了測試異步方法,使用XCTestExpectation
方法使你的測試等待異步操作完成。</p>
<p>異步測試通常是比較花時間的,所以應該將它們與那些可以很快就執行完的測試方法分開。</p>
<p>從+菜單選擇"新建單元測試目標…",將測試命名為HalfTunesSlowTests。在import
聲明后導入HalfTunes:</p>
<pre><code>@testable import HalfTunes</code></pre>
<p>在這個類的測試中將使用默認會話來將請求發送給蘋果的服務器,所以聲明一個SUT對象,并在setup()
方法中創建它,在teardown()
方法中釋放它:</p>
<pre>var sessionUnderTest: URLSession! override func setUp() { super.setUp() sessionUnderTest = URLSession(configuration: URLSessionConfiguration.default) } override func tearDown() { sessionUnderTest = nil super.tearDown() }
</pre>用下面的異步測試方法替換<code>testExample()</code>:
<pre>// Asynchronous test: success fast, failure slow func testValidCallToiTunesGetsHTTPStatusCode200() { // given let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba") // 1 let promise = expectation(description: "Status code: 200") // when let dataTask = sessionUnderTest.dataTask(with: url!) { data, 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 waitForExpectations(timeout: 5, handler: nil) }
</pre><p>這個測試方法發送一個有效的查詢給iTunes并期望返回的狀態代碼是200。這里的大多數代碼和在應用程序中編寫的代碼相同,除了下面這些額外的代碼行:</p>
<ol><li>expectiation(_:)
返回一個XCTestExpectation
對象,它被賦值給promise
變量。該對象的其他常用名稱是expectation
和future
。參數description
描述了期望發生的結果。
<li>為了和描述匹配,在異步方法完成處理函數的成功條件分支中調用promise.fulfill()
。
<li>waitForExpectations(_: handler:)
保證測試在所有的期望被達成前保持運行,或者直到超時結束,以二者先發生者為準。</ol>
<p>執行測試。如果你連接到了互聯網,在模擬器啟動后,測試應該需要大約一秒鐘成功返回。(譯注:這個方法用URLSession
執行了和程序中類似的操作,但沒有直接測試程序中的代碼)</p>
<h3>讓失敗發生得更快</h3>
<p>失敗是有害的,但不必總保持失敗。在這里將討論如果測試失敗了,如何快速找出原因。把時間節省下來可以更好地浪費在臉譜網上。:]</p>
<p>要修改測試,以使異步操作返回失敗結果,只需要從網址中的“iTunes”里刪除“s”
<pre><code>leturl=URL(string:"https://itune.apple.com/search?media=music&entity=song&term=abba")</code></pre>
執行測試:測試會返回失敗,但它需要等待整個超時時間間隔!這是因為它的期望是請求會成功,就是在調用promise.fulfill()
的地方。由于請求失敗,測試只有在超時過期了才結束。(譯注:雖然測試已經失敗了(XCTFail()
被調用),但因為promise.fulfill()
沒有被調用,方法不會立刻結束。)
<p>通過更改期望,可以使測試失敗情況發生的更快,而不必等待請求成功。只要等到異步方法的完成處理方法被調用就可以了。也就是當應用程序收到來自服務器的響應(OK或錯誤)時。這滿足了期望,然后測試里可以接著檢查請求是否成功。</p>
<p>要查看這是如何工作的,你將創建一個新的測試。首先,撤銷上面對URL的更改來修復測試,然后在類中添加下面的測試方法:</p>
<pre>```// Asynchronous test: faster fail
func testCallToiTunesCompletes() {
// given
let url = URL(string: "https://itune.apple.com/search?media=music&entity=song&term=abba")
// 1
let promise = expectation(description: "Completion handler invoked")
var statusCode: Int?
var responseError: Error?
// when
let dataTask = sessionUnderTest.dataTask(with: url!) { data, response, error in
statusCode = (response as? HTTPURLResponse)?.statusCode
responseError = error
// 2
promise.fulfill()
}
dataTask.resume()
// 3
waitForExpectations(timeout: 5, handler: nil)
// then
XCTAssertNil(responseError)
XCTAssertEqual(statusCode, 200)
}</pre> 這里的關鍵是,單單進入完成處理程序就滿足了期望,這需要大約一秒鐘。如果請求失敗了,則then部分的斷言會失敗。 <p>執行測試:它現在需要大約一秒鐘就會失敗返回。失敗是因為查詢請求失敗了,而不是因為測試執行超時。 修復
url```,然后再次執行測試并確認現在結果是成功的。</p>
偽對象和交互
<p>異步測試是為了確認代碼中調用異步API的輸入參數是正確的。你也可能還想要測試接收urlsession
的返回值的代碼也能正常工作,或者程序可以正確地更新UserDefaults或CloudKit數據庫。</p><p>
大多數應用都要同系統或者庫函數對象打交道,這些對象不受你控制。如果測試方法同這些對象進行交互,那么執行起來可能會很慢或者結果不具有可重復性。這就違反了FIRST原則中的兩個,執行的速度要快和具有可重復性。使用輸入樁(stubs)或通過更新模擬對象(mock objects)來偽造交互是常用的替代方法。</p>
當你的代碼依賴于某個系統或庫時,可以使用偽裝,即創建一個偽對象來扮演相關的系統或庫,并把它注入到你的代碼中。Jon Reid寫的依賴注入描述了幾種可以達到這個目的的方法。
</p>
來自樁(stub)的偽輸入
<p>在這個測試中,你會通過檢查searchResults.count
的值來判斷程序的<code>updateSearchResults(_:)</code>方法是否正確地解析了會話所下載的數據。在這里,SUT是視圖控制器,你會用樁和一些預先下載的數據來偽造會話。</p><p>
從+菜單選擇“新的單元測試目標…”。把它命名為HalfTunesFakeTests。在import
語句下方導入HalfTunes :</p>
<pre><code>@testable import HalfTunes</p></code></pre><p>聲明SUT,在setup()
中創建它,并在teardown()
中釋放:</p><pre>var controllerUnderTest: SearchViewController! override func setUp() { super.setUp() controllerUnderTest = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController() as! SearchViewController! } override func tearDown() { controllerUnderTest = nil super.tearDown() }
</pre>
<pre>注:這里SUT是視圖控制器。因為HalfTunes有視圖控制器過于龐大的問題-所有的工作都在SearchViewController.swift中進行。將網絡代碼移動到單獨的模塊可以減輕這個問題,也會使測試變得更容易。</pre><p>
接下來,你將需要產生一些JSON樣本數據來由偽會話返回給你的測試方法。數據有幾條就夠了,所以在發給iTunes的URL字符串后面加上“& limit=3”來限制返回結果:</p>
<p>https://itunes.apple.com/search?media=music&entity=song&term=abba&limit=3</p>
<p>拷貝粘貼這個URL到瀏覽器中。這會下載1.txt或一個類似的文件。預覽確認這是一個JSON文件,然后將它改名為abbaData.json并添加到HalfTunesFakeTests組中。</p>
<p>HalfTunes工程中包含支持文件DHURLSessionMock.swift。其中定義了一個簡單的協議——DHURLSession
。這個協議中包含兩個使用URL
或URLRequest
來創建數據任務的方法(stubs)。它還定義了一個實現了該協議的URLSessionMock
。URLSessionMock
提供一個構造器,它可以根據你提供的數據(data, response, error)創建一個模擬URLSession
對象。</p>
<p>如下所示,在setup()
中創建SUT后,建立偽數據和響應,并建立偽會話對象。</p>
<pre>let testBundle = Bundle(for: type(of: self)) let path = testBundle.path(forResource: "abbaData", ofType: "json") let data = try? Data(contentsOf: URL(fileURLWithPath: path!), options: .alwaysMapped) let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba") let urlResponse = HTTPURLResponse(url: url!, statusCode: 200, httpVersion: nil, headerFields: nil) let sessionMock = URLSessionMock(data: data, response: urlResponse, error: nil)
</pre><p>在setup()
的結尾,將偽會話作為SUT的屬性注入到應用程序中:</p>
<pre>controllerUnderTest.defaultSession = sessionMock
</pre>
<pre>注:你將在你的測試中直接使用偽會話,但向你展示了如何進行注入,以便在未來的測試中可以調用SUT的方法來使用視圖控制器的defaultSession
屬性。</pre><p>現在你準備好了寫一個測試來檢查對<code>updateSearchResults(_:)</code>的調用是否正確解析了所提供的偽數據。用下面的代碼替換<code>testExample()</code>:</p>
<pre>// Fake URLSession with DHURLSession protocol and stubs func test_UpdateSearchResults_ParsesData() { // given let promise = expectation(description: "Status code: 200") // when XCTAssertEqual(controllerUnderTest?.searchResults.count, 0, "searchResults should be empty before the data task runs") let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba") let dataTask = controllerUnderTest?.defaultSession.dataTask(with: url!) { data, response, error in // if HTTP request is successful, call updateSearchResults(\_:) which parses the response data into Tracks if let error = error { print(error.localizedDescription) } else if let httpResponse = response as? HTTPURLResponse { if httpResponse.statusCode == 200 { promise.fulfill() self.controllerUnderTest?.updateSearchResults(data) } } } dataTask?.resume() waitForExpectations(timeout: 5, handler: nil) // then XCTAssertEqual(controllerUnderTest?.searchResults.count, 3, "Didn't parse 3 items from fake response") }
</pre>
<p>因為樁提供的是一個異步方法,這個測試也仍然必須寫成異步的。(譯注:這里的樁是sessionMock
)</p>
<p>when斷言的條件是searchResults
在數據任務運行前為空-這應該是真的,因為你在setup()
中創造了一個全新的SUT。</p>
<p>偽數據包含了三個音軌對象的JSON數據,所以then斷言的條件是,視圖控制器的搜索結果數組包含三個項目。</p>
<p>執行測試。它應該很快就返回成功,因為沒有任何真正的網絡連接。</p>
對模擬對象的假更新
<p>以前的測試使用存根從偽對象提供輸入。接下來,你可以使用一個模擬對象來測試你的代碼可以正確地更新UserDefaults。</p>
<p>重新打開BullsEye項目。該應用程序有兩種玩法:用戶要么移動滑塊來匹配目標值或根據滑塊位置猜測目標值。右下角的分段控件可以切換游戲玩法和更新gameStyle用戶默認值以保持一致。</p>
<p>你的下一個測試將檢查應用程序正確地更新了gameStyle
的默認值。</p>
<p>在測試導航欄,點擊“新的單元測試的目標”,將測試命名為BllsEyeMockTests。在import
語句下面添加以下內容:</p>
<pre>@testable import BullsEye class MockUserDefaults: UserDefaults { var gameStyleChanged = 0 override func set(_ value: Int, forKey defaultName: String) { if defaultName == "gameStyle" { gameStyleChanged += 1 } } }
</pre>
MockUserDefaults
覆蓋了set(_:forKey:)
方法來增大gameStyleChanged
標志。你經常會看到類似的測試中設置的是布爾變量,但使用遞增整數可以給你更多的靈活性,例如,您的測試可以檢查方法是否正好被調用了一次。
在BullsEyeMockTests
中聲明SUT和mock對象:
<pre>var controllerUnderTest: ViewController! var mockUserDefaults: MockUserDefaults!
</pre>
在setup()中創建SUT和模擬對象,然后注入模擬對象作為SUT的屬性:
<pre>controllerUnderTest = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController() as! ViewController! mockUserDefaults = MockUserDefaults(suiteName: "testing")! controllerUnderTest.defaults = mockUserDefaults
</pre>
釋放SUT和teardown()中的模擬對象:
<pre>controllerundertest = nil mockuserdefaults = nil
</pre>
將testexample()
替換為:
<pre>// Mock to test interaction with UserDefaults func testGameStyleCanBeChanged() { // given let segmentedControl = UISegmentedControl() // when XCTAssertEqual(mockUserDefaults.gameStyleChanged, 0, "gameStyleChanged should be 0 before sendActions") segmentedControl.addTarget(controllerUnderTest, action: #selector(ViewController.chooseGameStyle(_:)), for: .valueChanged) segmentedControl.sendActions(for: .valueChanged) // then XCTAssertEqual(mockUserDefaults.gameStyleChanged, 1, "gameStyle user default wasn't changed") }
</pre>
<p>when斷言的條件是在測試方法taps分段控件前gamestylechanged
標志為0。所以如果then斷言也為真,意味著<code>set(_:forKey:)</code>正好被調用了一次。運行測試,結果應該為成功。</p>
在Xcode中測試用戶交互(UI)
<p>在Xcode 7中引入了UI測試,它能讓你通過記錄你的UI操作來創建UI測試。UI測試通過查找一個應用程序的UI對象進行查詢,合成事件,然后將它們發送到這些對象。API使您能夠檢查UI對象的屬性和狀態,以便將它們與預期狀態進行比較。<p>
打開BullEyes項目,在該項目的測試導航,添加一個新的UI測試目標,然后接受默認名稱BullsEyeUITests。
在BullsEyeUITests類頂部添加屬性:
<pre><code>var app: XCUIApplication!</code></pre><p>在setup()
中將語句<pre><code>XCUIApplication().launch()</code></pre>替換為:</p>
<pre>app = XCUIApplication() app.launch()
</pre></p><p>將<code>testExample()</code> 改名為<code>testGameStyleSwitch()</code>。
在<code> testGameStyleSwitch()</code>方法中開始一個新行,然后單擊編輯器窗口下方的紅色錄音按鈕:</p>
當應用在模擬器中啟動后,點擊游戲風格切換開關和對應的頂部標簽。然后點擊Xcode記錄按鈕停止錄音。
在<code>testGameStyleSwitch()</code>方法中會新生成如下三行:
<pre>```let app = XCUIApplication()
app.buttons["Slide"].tap()
app.staticTexts["Get as close as you can to: "].tap()
如果有其他內容,刪除它們。第一行重復了你在<code>setup()</code>中創建的屬性,你現在也不需要點擊任何東西,所以刪除第一行以及第二行和第三行結尾處的<code>.tap()</code>。在代碼中打開<code>["Slide"]</code>右側的小下拉菜單,然后選擇<code>segmentedControls.buttons["Slide"]</code>
現在代碼的樣子會變為:
<pre>```app.segmentedControls.buttons["Slide"]
app.staticTexts["Get as close as you can to: "]```</pre>
修改它以創建given部分:
<pre>```// 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: "]```</pre>現在您等到了兩個按鈕的名稱和兩個可能的頂級標簽。接著請添加以下內容:
<pre>```// 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)
}```</pre>這段代碼檢查每個按鈕被選中時對應的正確的標簽是否存在。執行測試-所有的斷言應該顯示成功。</pre></p>
##性能測試
根據[蘋果的文檔](https://developer.apple.com/library/prerelease/content/documentation/DeveloperTools/Conceptual/testing_with_xcode/chapters/04-writing_tests.html#//apple_ref/doc/uid/TP40014132-CH4-SW8):性能測試會先取得你要評估的代碼塊,然后將它運行十次。收集均執行時間的平均值和標準差。這些單獨測量值的平均值形成的結果可以與基準進行比較,以評估測試成功或失敗。
<p>寫一個性能測試很簡單:只需把你想測量到代碼放入<code>measure()</code>方法中的閉包中。</p>讓我們實際做一下。再次打開HalfTunes項目,在HalfTunesFakeTests中將<code> testPerformanceExample ()</code>替換為:
<pre>```// Performance
func test_StartDownload_Performance() {
let track = Track(name: "Waterloo", artist: "ABBA",
previewUrl: "http://a821.phobos.apple.com/us/r30/Music/d7/ba/ce/mzm.vsyjlsff.aac.p.m4a")
measure {
self.controllerUnderTest?.startDownload(track)
}
}```</pre>運行測試,然后如下圖所示,單擊```measure()```方法結尾處的圖標來查看統計結果。

單擊“設置基線”,然后再次運行性能測試并查看結果-它可能比基線更好也可能更差。編輯按鈕讓您可以使用這個新的結果來重置基線。
<p>基線是根據每個設備的配置來存儲的,所以你可以將相同的測試在不同的設備上執行,并根據不同的處理器,內存等配置來設定不同的基線。</p>
<p>任何時候做可能會影響正在測試的方法的性能的更改,都請再次運行性能測試,來查看與基線比較,性能的變化。</p>
<h2>代碼覆蓋</h2><p>代碼覆蓋工具告訴你哪些應用程序的代碼真正被你的測試執行了,這樣你就可以知道程序代碼的哪些部分還沒有被測到。</p>
<pre>注:在啟用代碼覆蓋時,是否應該運行性能測試?[蘋果的文檔](https://developer.apple.com/library/prerelease/content/documentation/DeveloperTools/Conceptual/testing_with_xcode/chapters/07-code_coverage.html#//apple_ref/doc/uid/TP40014132-CH15-SW1)說:代碼覆蓋率數據收集會帶來性能上的損失…以線性方式影響代碼執行,這樣當它啟用時,性能測試結果與測試運行到測試運行保持一致。但是,您應該考慮是否在您在測試中嚴格評估例程的性能時啟用代碼覆蓋率。</pre>
為了啟用代碼覆蓋、編輯方案(scheme)中的“測試動作”的并選中代碼覆蓋項:(譯注:使用 ?+< 開始編輯方案)

運行所有的測試(?+U),然后打開報告導航欄(?+8)。按時間選擇,選擇該列表中的第一項,然后選擇“覆蓋”選項卡:

<p>單擊三角形標志可以看到SearchViewController.swift中的函數列表:</p>

將鼠標移動到<code>updateSearchResults(_:)</code>后面的藍條上可以看到覆蓋率為71.88%。
<p>單擊此功能的箭頭按鈕來打開源文件,然后定位該函數。當鼠標越過右側邊欄的覆蓋注釋時,代碼段會高亮顯示綠色或紅色:</p>

覆蓋注釋顯示多少次試驗打每個代碼段;沒有被自行的部分被用紅色標出。如你所期望的,```for```循環跑了3次,但出錯處理路徑上的代碼都沒有被執行。要增對這些功能的覆蓋,你可以復制abbadata.json,然后編輯出不同的錯誤,例如,將"results"改為“result”,這樣可以得到一個能夠覆蓋<code>print("Results key not found in dictionary")</code>的測試。</p>
###100%覆蓋?
<p>應該努力爭取達到100%的代碼覆蓋率么?百度一下“100%單元測試覆蓋率”,你會發現一系列的贊成和反對的理由,和對“100%覆蓋”的定義的爭論。反對方說最后10-15%是不值得努力的。支持方爭論說最后的10-15%是最重要的,*因為*它難以測試。百度“hard to unit test bad design"去查找一篇很有說服力的文章[無法驗證的代碼是一個更深層次的設計問題的標志](https://www.toptal.com/qa/how-to-write-testable-code-and-why-it-matters)。進一步思考很可能會得出這樣的結論:正確的方向是[測試驅動開發](http://qualitycoding.org/tdd-sample-archives/)。</p>
##下一步做什么?
<p>現在你已經掌握了一些用于編寫項目測試的優秀工具。我希望這個iOS單元測試和UI測試教程給了你信心去測試所有的東西!
您可以在[zip文件](https://koenig-media.raywenderlich.com/uploads/2016/12/Finished-3.zip)中找到已完成的項目。下面是一些深入研究的資源:</p>
<li>現在你已經會為你的項目寫測試了。下一步是自動化:持續集成和持續交付。閱讀蘋果的使用Xcode服務器和xcodebuild[自動化測試過程](https://developer.apple.com/library/content/documentation/DeveloperTools/Conceptual/testing_with_xcode/chapters/08-automation.html#//apple_ref/doc/uid/TP40014132-CH7-SW1),和來自維基百科的[持續交付文章](https://en.wikipedia.org/wiki/Continuous_delivery),這些文章借鑒了[ThoughtWorks](https://www.thoughtworks.com/continuous-delivery)的專家的專業知識。</p>
<li>[在Swift Playgounds中使用TDD](http://initwithstyle.net/2015/11/tdd-in-swift-playgrounds/) 介紹了在Playgounds中使用```XCTestObservationCenter```執行```XCTestCase```單元測試。你可以在Playgounds中開發你的項目代碼和編寫測試,然后再把兩者轉移到你的應用中去。
<li>[手表應用:我們如何測試它們?](https://realm.io/news/cmduconf-boris-bugling-how-test-watch-apps/)來自[CMD + U](http://www.cmduconf.com/)會議展示了如何使用[PivotalCoreKit](https://github.com/pivotal/PivotalCoreKit)測試WatchOS應用。
<li>如果你已經有一個應用程序,但還沒有為它寫過測試,你可能要參考米迦勒的[如何高效地工作在老舊代碼上](https://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052/ref=sr_1_1?s=books&ie=UTF8&qid=1481511568&sr=1-1),因為沒有測試程序的代碼就*是*老舊代碼!</p><p>
<li>喬恩瑞德的高質量編碼示例應用程序文檔是一個學習更多的關于[測試驅動開發](http://qualitycoding.org/tdd-sample-archives/)的好地方。
</p><p>如果您對本教程有任何問題或意見,請加入下面的論壇討論。:]</p>
###團隊
www.raywenderlich.com的每個教程都是由我們的專職團隊完成,以確保其符合我們的高質量標準。創建本教程的團隊成員是:

<b>來自譯者:</b>如果你認真讀到了這里,并按教程完成了里面的示例,我相信你一定和我一樣收獲良多。測試對我而言一直是深覺重要又覺得無從下手的一個課題,這篇文章雖然沒有也無法給出所有我們想要的答案,但無疑打開了一扇大門。<p>如有任何和本文或iOS測試相關的問題,歡迎留言,謝謝。</p>