甚至情況更差一點,就是找來找去也找不到錯在哪裡。不論你是程式新手或者已經有點經驗的開發者,定期編寫單元測試 (unit testing) 會讓你的程式碼更可靠、安全,當問題出現的時候更能除錯就更容易。
幸運地,Xcode 7 和 Swift 都支援單元測試。雖然使用單元測試不代表錯誤不會出現,但把程式碼分隔為細小的單元,從而逐一檢測每個單元都是正確地執行,此舉有助除錯偵測。
顧名思義,單元測試是逐一為每個單元的程式碼去建立指定的功能測試,以確保每一個單元都能通過。而成功通過的單元程式碼會在旁邊顯示綠色標誌。若果因為某些因素而測試失敗,Xcode 就會直接顯示測試結果為失敗 (failed)。這個標示能讓你找到有問題的程式碼的位置,也方便了追查出錯的原因。
範例專案概要
首先,先我為你準備好的Starting Project下載。這是一個十分簡單的百份比運算 App(舉例:80 x 10%= 8 )。
這個百份比運算器比較簡單。你所需要去看的文檔就只有 ViewController.swift。文檔中,簡潔的程式碼附帶了註釋,讀起來更清晰明白。
文檔中有五個 IBOutlets:一個是屏幕上標題之外的每一個 UIElement;其餘分別是在兩旁的浮動塊 (slider),每邊兩個的 IBActions。每個 IBAction 的名字都準確地說明了操作方法和指令。而當兩個浮動塊,其中一個的值有所改變,其餘的百份比或數字值都會作出調整。
此外,文檔中還可以發現兩個簡單的函數 “updateLabels()” 和 “percentage()”,它們的作用完全是合符開發者的期望:前者是當一個浮動塊作出改動時為標號作出更新,而後者提取兩個浮點值 ,經百份比運算後把結果回傳。
利用模擬器執行一下App。首先,一切看似正常。但當你開始更改數字,你會發現運算出來的答案是錯誤的。要把錯處找出來,我們可以把程式碼區分為不同的單元,並逐一進行測試,以檢查它們是否合符預期地運作。這個動作不能除錯,但絕對有助縮少搜索的範圍。
在我創建這個專案時,在專案細項選填畫面裡勾選了包含單元測試檔案 (include Unit Tests) 。(若果你想自行加入檔案,選擇 File > New > File > Unit test Case Class under iOS Source)。在這個範例專案,Xcode已經為你準備好這些檔案,並可以在 project navigator 面版中的 “PercentageCalculatorTests” 文件夾裡找到。
在 PercentageCalculatorTests
類別中,PercentageCalculatorTests.swift
為開發者提供了四種測試方法。其中兩個測試方法的檔案是可以被刪除的(測試檔案皆以 test
字為開首,”…Example” 作結尾,便於辨認;以及在欄距有鑽石型圖標作標示)。另外 setUp()
和 tearDown()
,這兩個屬於特別調理板方法,每次每個測試方法被執行的之前和之後都會被呼叫出來。
開始動手寫單元測試
現在,是時候讓你寫下第一個單元測試方法。在這個教程中,我們只會針對 ViewController
作出測試,我們會為它在 PercentageCalculatorTests
類別中新增一個實體。
class PercentageCalculatorTests: XCTestCase {
var vc: ViewController!
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
}
PercentageCalculatorTests
類別是 XCTestCase
的子類別,來自 XCTest 框架。而在 XCTestCase
子類別下的每一個實體都是負責測試 你的專案的某一個指定部份,舉例說指定函數。
在 setup 方法中以 vc
為例。在每個測試之前 setUp()
都會被呼叫出來,然後你將會從 ViewController
的每個測試方法裡到得到一個「即時」實體。把 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
),若果透過表式測試得出來的評估是 true,XCTAssert
測試就是成功通過。相反,若果評估結果為 false 就為之失敗。
試試看!首先,把下列的程式碼加入至 testPercentageCalculator()
方法。然後移動你的滑鼠,將游標放在方法名稱左側欄的鑽石型圖標上,這時鑽石型圖標會轉變成執行圖標,點擊便開始測試。
func testPercentageCalculator() {
XCTAssert(true)
}
若果一切順利進行,測試得到成功通過就會在方法旁邊顯示出一個綠色的打勾符號。
驗證百份比計算器
進入下一步,正式開始。若要測試 percentage()
方法,我們需要使用 vc
去呼叫此方法。給它兩個浮點值,同樣以 50 為例子,並把輸出結果存儲為 p
常數。在這個案例中,p
應該是等於 25(50 x 50% = 25)。使用 XCTAssert(p == 25)
去檢查這個案例並執行測試方法。以下列程式碼取代 testPercentageCalculator()
方法:
func testPercentageCalculator() {
// Should be 25
let p = vc.percentage(50, 50)
XCTAssert(p == 25)
}
成功通過這個測試,意味著 ViewController
裡的 percentage()
函數發揮其功能,那麼我們可以繼續往其他地方偵查錯誤存在的位置,或許可以試一試往 updateLabels()
方法去找。
驗證標記
現在新增一個名為 testLabelValuesShowedProperly()
的測試方法,它可以驗證標記能否把適當的文字顯示出來。同樣地,以 ViewController 呼叫 updateLabels()
方法,以檢查 text
屬性是否每次都能顯示出我們預期的文字。
這裡要注意一下,你需要給 XCTAssert
函數加入一個新的參數:一個以 string 型態的訊息。這是很方便的,因為我們擁有多個值需要透過測試去完成(三個 XCTAssert 呼叫)。假若測試是不成功,這個訊息便會告知我們確實出錯的地方。
func testLabelValuesShowedProperly() {
vc.updateLabels(Float(80.0), Float(50.0), Float(40.0))
// The labels should now display 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 檔案裡建立這些標記,所以當視圖被加載時它們便會被實例化 (instantiated),但由於單元測試的 loadView()
方法並沒有被觸發,標記因此不能被建立,所以他們是 nil
。一個可行的解決辦法是呼叫 vc.loadView()
,但 Apple 卻不建議在文檔中這樣做,因為當物件不斷加載再加載或或會導致記憶體洩流 (memory leaks) 的發生。
取而代之,你應該存取 vc
的 view
屬性,它可以觸發所有所需要的方法,而不只是單純是 loadView()
方法。先把 testLabelValuesShowedProperly()
的程式碼作出更新。
func testLabelValuesShowedProperly() {
let _ = vc.view
vc.updateLabels(Float(80.0), Float(50.0), Float(40.0))
// The labels should now display 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")
}
使用底線 ( _ ) 取代成常數名字。這是因為我們實際上不需要也不會用得上它。基本上它只是告知編譯程式「只是假裝存取視圖及觸發方法」。
執行測試。(如果你想在我們的測試類別裡執行所有的測試,你可以勾選 “class PercentageCalculatorTests” 的小方格)
錯誤處理
如你所見測試是失敗的!這個方法裡輸入的詳細錯誤訊息,能使我們快速地辨認出錯誤的潛在原因。這個測試是告訴我們 resultsLabel 不能顯示出正確的文字。接下來,前往 ViewController 並看看在哪裡設定這些標記的文字值。仔細研究 ViewController.swift
內 updateLabels()
的程式碼,我們發現了錯誤原因:
self.resultLabel.text = "\(rV + 10)"
應該是:
self.resultLabel.text = "\(rV)"
再次編譯與執行測試。問題得到解決,一切都得到順利進行。
結語
在這個教程,你嘗試到在 Xcode 使用單元測試,並了解它是如何協助你尋找隱藏在程式碼中的錯誤。除了避免錯誤發生,單元測試還可以用作效能測試 (Performance Testing) 和 異步測試 (Asynchronous Testing)。還有,或許你會有興趣了解一下 UI 自動化測試 (UI Testing),透過錄製的方式記錄 App 的實際操作,並讓開發者檢視其效能表現。關於 UI 自動化測試,可以瀏覽 WWDC 視頻。
本教程的完整專案範例可以在 GitHub 下載。
如果你對單完測試或本教程有任何疑問或困難,歡迎留言查詢。