Xcode7中用Swift做單元測試
每個 iOS 程序員都要時不時的為他們的 app 做 debug。除非你是那種超級大牛,否則你肯定體驗過查了無數個小時的 bug ***才發現那僅僅是個簡單的語法錯誤時那種油然而生的絕望感。或者更糟:你根本就沒發現那些 bug。無論你是編程新手,還是開發過很多 app 的老司機,例行的寫寫單元測試會讓你的代碼更可靠,更安全,更容易 debug!
你很走運,Xcode 7 和 Swift 支持單元測試。盡管單元測試不保證(有了它你就會寫出)絕對沒有 bug 的 app,它還是一種能讓你驗證每段代碼是否如期工作,并讓 debug 過程更加便利。
正如其名,在單元測試中你要為某段代碼單元創建一些小規模的、針對其某個特性的測試,然后確保每個代碼單元都能通過這些測試。如果通過的話,它的旁邊會出現一個綠色小標志,而如果因故測試不通過, Xcode 會把該測試標記為 “failed”。這就提示你去查看代碼,找出失敗原因。
演示項目概覽
首先下載這個我為你準備的 starting project。一個短小精悍的 app:它會對一個給定的數字和百分比做一個乘法計算。(比如80的10%是8。)
這個 PercentageCalculator 項目非常簡單。你唯一需要關注的就是 ViewController.swift 這個文件。里面的代碼都標記了注釋,很容易理解。
有 5 個 IBOutlets:每一個都對應了屏幕上一個 UIElement,除 title(標題)之外,還有 2 個 slider 對應 2 個 IBActions。每個 IBAction 的方法名都精確描述了其用途及將要執行的操作。當一個 slider 值改變時,其對應著的百分比或數字的值也會隨之改變。
還有兩個簡單的函數 “updateLabels()” 和 “percentage()” 做了符合期待的事情:當一個 slider 改變時***個函數更新 label,第二個函數獲取兩個浮點數并返回百分比的計算結果。
在模擬器中運行 app。剛開始一切看起來都很正常。但當你開始改變數字時就會發現計算結果有問題。為找到 bug,我們將代碼分割成不同的單元,然后分別做測試,看看每個是否都如期運行。這不會解決 bug,但能縮小你的查找范圍。
我創建項目的時候,默認情況下會勾選創建一個 test 文件的選項(如果你想要手動加一個的話,在 iOS Source 下面選擇 select File > New > File > Unit Test Case Class)。我們的例子中 test 文件已經被 Xcode 自動創建出來,可以在項目導航欄中 “PercentageCalculatorTests” 文件夾中找到它。
在 PercentageCalculatorTests.swift 文件中,PercentageCalculatorTests 類里面已經為我們創建好了 4 個方法。其中 2 個是測試方法(test methods)的例子,你可以刪掉它們(它倆都以 test 關鍵字開頭,并且它們左邊的豎條中都有個方塊形圖標,名字也都以 “…Example” 結尾,所以你可以通過這些辨識出來它們是測試方法)。另外兩個方法,setUp() 和 tearDown() 是特殊的樣板方法(boilerplate methods),它們分別在每個測試方法被執行之前,和每個測試方法被執行之后被執行。
開始寫單元測試吧
現在是時候寫你的***個單元測試函數了!本教程我們只測試 ViewController 類,需要在 PercentageCalculatorTests 中添加一個它的實例。
- class PercentageCalculatorTests: XCTestCase {
- var vc: ViewController!
- override func setUp() {
- super.setUp()
- // 這里寫setup的代碼。本class里每個測試函數被調用之前該方法都會被先調用。
- }
- override func tearDown() {
- // 這里寫teardown的代碼。本class里每個測試函數被調用之后該方法都會被調用。
- super.tearDown()
- }
- }
PercentageCalculatorTests 是一個 XCTestCase 的子類,后者被打包在 XCTest 框架中。每一個 XCTestCase 子類的實例都負責對你項目的某個特定部分做測試,比如對一個特性做測試。
在 setup 方法中實例化一個 vc。這樣對每一個測試方法你都會得到一個“全新的” ViewController 實例,因為在每個測試方法執行前 setUp() 都會被調用一次。把 setUp() 方法修改如下:
- override func setUp() {
- super.setUp()
- let storyboard = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle())
- vc = storyboard.instantiateInitialViewController() as! ViewController
- }
現在你應該記得所有的測試方法的名字都要以 test 關鍵字開頭,否則 Xcode 不會識別。添加一個新的 testPercentageCalculator() 測試方法,來驗證一下 ViewController 中的 percentage() 工作是否正常。
- func testPercentageCalculator() {
- }
單元測試中你要去檢查某段代碼是否如你所愿的那樣工作。待測試的代碼段一般都只有幾行,典型情況是你只需要測試一個方法或者一個函數。單元測試是這樣去做的:你給某個代碼單元一個輸入值,讓這個值過一遍這段代碼,然后檢查一下輸出的值是否和預期的一樣。
與“我們期望的那個值”做比較的這部分由 XCTAssert 函數來處理。最簡單的 XCTAssert 函數是XCTAssert(expression: BooleanType)。這個函數要求一個布爾表達式(類似于 5>3,8.90 == 8.90或者 true 這種),隨后如果表達式為真則讓測試通過,否則認為測試失敗。
嘗試一下!首先給 testPercentageCalculator() 方法加添加下面一行。然后把光標移到方法名左邊側欄的那個方塊圖標上,停下光標之后方塊圖標變成了一個執行光標,點擊一下就開始了測試。
- func testPercentageCalculator() {
- XCTAssert(true)
- }
如果一切順利,則測試通過,方法左邊會出現一個綠色檢測標。
驗證百分比計算
現在來真的:測試 percentage() 方法!用 ViewController 的一個實例 - vc 屬性來調用這個方法。給這個方法兩個浮點數,比如 50 和 50,然后把結果存儲到常量 p 中。這個例子中 p 應該是 25(50 的 50% 是 25)。然后用 XCTAssert(p == 25) 檢測一下是不是這樣,執行測試方法。把 testPercentageCalculator() 改成這樣:
- func testPercentageCalculator() {
- // 應該是25
- let p = vc.percentage(50, 50)
- XCTAssert(p == 25)
- }
測試成功了,這意味著 ViewController 的 percentage() 函數工作正常,我們應該在其他的地方繼續尋找 bug。也許 bug 在 updateLabels() 里面?
驗證Labels
現在添加一個新的測試方法 testLabelValuesShowedProperly() 來驗證一下 label 能不能正確的顯示 text。和之前一樣,調用 ViewController 的一個方法 - 這回是 updateLabels() - 然后看看每個標簽的 text 屬性和我們期望的那個 text 是否相同。
注意到你要給 XCTAssert 函數傳一個新的參數:一個 string 類型的消息。這對我們這次要對多個值做檢查(調用三次 XCTAssert )來完成測試而言就會很方便。如果測試失敗,這條消息就會指名我們具體是哪里錯了。
- func testLabelValuesShowedProperly() {
- vc.updateLabels(Float(80.0), Float(50.0), Float(40.0))
- // labels應該顯示80, 50 and 40
- XCTAssert(vc.numberLabel.text == "80.0", "numberLabel doesn't show the right text")
- XCTAssert(vc.percentageLabel.text == "50.0%", "percentageLabel doesn't show the right text")
- XCTAssert(vc.resultLabel.text == "40.0", "resultLabel doesn't show the right text")
- }
你嘗試執行這個測試方法時,會收到編譯器的錯誤提示:numberLabel,percentageLabel 和 resultsLabel 是 nil。怎么回事呢?
我是在 storyboard 文件中創建了這些 labels 的,因此只有當 view 被加載之后(loaded)它們才會被初始化,然而由于對單元測試來說 loadView() 方法不會被觸發,所以這些 labels 沒有被創建,只能是 nil。一種可能的方法是通過調用 vc.loadView() 來解決,但是 Apple 在它的文檔中并不推薦你這么做,因為當已經被加載的對象又被加載一次的話可能會引起內存泄露。
正確的方法是你應該先訪問一下 vc 的 view 這個屬性,這會讓 vc 反過來觸發所有相應的方法,不僅僅包括 loadView()。把 testLabelValuesShowedProperly() 改成這樣:
- func testLabelValuesShowedProperly() {
- let _ = vc.view
- vc.updateLabels(Float(80.0), Float(50.0), Float(40.0))
- // labels應該顯示80, 50 and 40
- XCTAssert(vc.numberLabel.text == "80.0", "numberLabel doesn't show the right text")
- XCTAssert(vc.percentageLabel.text == "50.0%", "percentageLabel doesn't show the right text")
- XCTAssert(vc.resultLabel.text == "40.0", "resultLabel doesn't show the right text")
- }
注意到下劃線(_)忽略了常量的名字。因為我們實際上并不需要用到這個 view。加下劃線就是告訴編譯器“你假裝訪問一下這個 view,把相應的方法觸發就行。”
執行測試。(如果想一并執行我們test類的所有測試,你還可以點擊 “class PercentageCalculatorTests” 旁邊的那個方塊)。
我們來修Bug
如你所見,測試失敗了!我們給 XCTAssert 方法傳入的錯誤細節消息幫助我們快速識別出引起 bug 的可能原因。這次測試告訴我們 resultsLabel 沒有顯示出正確的文本,所以我們進到 ViewController 里看看對這些 label 的 text 值是在那里被設置的。仔細看了 ViewController.swift 的 updateLabels() 代碼之后,我們發現了 bug 的原因:
- self.resultLabel.text = "\(rV + 10)"
應該是:
- self.resultLabel.text = "\(rV)"
更新代碼之后再運行一次測試,一切都應該正常了!
結論
本篇教程中你學到了 Xcode 中的單元測試的相關內容,以及它怎樣能夠幫你找到代碼中的 bug。除了預防 bug 之外,單元測試還可以用來做性能測試和異步測試。還可能讓你感興趣的是UI測試,你可以錄制下你在 app 上做出的動作來測試你的 app 在實際使用情景下是如何表現的。如果聽起來覺得感興趣,那一定要看看這個講 UI 測試的 WWDC視頻。
項目的最終版本可以在 Github上下載。
如果你有關于 UI 測試的任何問題,或者學習本教程中遇到了困難,請在評論中點我!
作者介紹:Maxime Defauw 是一個有經驗的程序員,在 App Store 和 Google Play store 上發布過多個 app。他今年 16 歲,居住在比利時。最近他在 San Francisco 舉行的 WWDC15 上獲得了 Apple 的獎學金。Max 熟練掌握 Objective-C,C,C#,現在是 Swift。不碼代碼的時候他一般在曲棍球場或者高爾夫球場上。在 Twitter 上 @MaximeDefauw 粉他。