測試驅動開發(TDD)是極限編程的重要特點,它以不斷的測試推動代碼的開發,既簡化了代碼,又保證了軟件質量。
測試驅動開發的基本思想就是在開發功能代碼之前,先編寫測試代碼。也就是說在明確要開發某個功能后,首先思考如何對這個功能進行測試,并完成測試代碼的編寫,然后編寫相關的代碼滿足這些測試用例。然后循環進行添加其他功能,直到完全部功能的開發。
OK,概括來說,TDD 的開發過程可以用上圖來描述:Red,Green,Refactor。
翻譯過來就是:
再詳細點,測試驅動開發的基本過程如下:
怎么樣,簡單吧~
簡單是簡單,但是很明顯的,開發前期,工作量絕對不是 1+1 那么簡單,那么是否該用 TDD 呢?對此,我不做過多的闡述。世上并沒有放之四海皆準的法則,TDD 好壞在于你的判斷,方法論的主體在于使用的人,本文并不會給你一個完美的答案,這需要你自己在實踐中取舍。接下去,我將列舉 TDD 目前公認的一些優缺點,以及使用原則,加深大家對 TDD 的理解。
TDD 開發的優點:
TDD 開發的缺點:
TDD 原則:
獨立測試:不同代碼的測試應該相互獨立,一個類對應一個測試類,一個函數對應一個測試函數。用例也應各自獨立,每個用例不能使用其他用例的結果數據,結果也不能依賴于用例執行順序。 一個角色:開發過程包含多種工作,如:編寫測試代碼、編寫產品代碼、代碼重構等。做不同的工作時,應專注于當前的角色,不要過多考慮其他方面的細節。
測試列表:代碼的功能點可能很多,并且需求可能是陸續出現的,任何階段想添加功能時,應把相關功能點加到測試列表中,然后才能繼續手頭工作,避免疏漏。
測試驅動:即利用測試來驅動開發,是TDD的核心。要實現某個功能,要編寫某個類或某個函數,應首先編寫測試代碼,明確這個類、這個函數如何使用,如何測試,然后在對其進行設計、編碼。
先寫斷言:編寫測試代碼時,應該首先編寫判斷代碼功能的斷言語句,然后編寫必要的輔助語句。
可測試性:產品代碼設計、開發時的應盡可能提高可測試性。每個代碼單元的功能應該比較單純,“各家自掃門前雪”,每個類、每個函數應該只做它該做的事,不要弄成大雜燴。尤其是增加新功能時,不要為了圖一時之便,隨便在原有代碼中添加功能。
及時重構:對結構不合理,重復等“味道”不好的代碼,在測試通過后,應及時進行重構。
小步前進:軟件開發是復雜性非常高的工作,小步前進是降低復雜性的好辦法。
?
看到這里,如果你還覺得,有必要體驗一把 TDD,那么接著往下看,我將通過一個簡單的例子,走一遍 TDD 開發的流程,加深大家對 TDD 的了解,也為 iOS 中應用 TDD 做個入門介紹。
Apple一直致力于在iOS開發中集成更加方便和可用的測試,在Xcode 5中,新的IDE和SDK引入了XCTest來替代原來的SenTestingKit,并且取消了新建工程時的“包括單元測試”的可選項(同樣待遇的還有使用ARC的可選項)。新工程將自動包含測試的target,并且相關框架也搭建完畢,可以說測試終于擺脫了iOS開發中“二等公民”的地位,現在已經變得和產品代碼一樣重要了。 —————— 喵神
簡單 Mark 下 TDD 在 Xcode 中的歷程:
既然 Xcode 為我們內置了這么方便的 XCTest,我們沒理由不好好使用阿~
接下去通過實現一個簡單的功能:把句子中每個單詞的首字母轉成大寫字母,來走一遍 TDD 的流程。話不多說,開車了~
這里創建一個常規的 iOS 工程,記得 “ Include Unit Tests”
即可,語言我們選擇 Swift
。
創建完畢后的工程目錄如下:
默認為我們創建了 TDDDemoTests.swift
文件,這里就是我們編寫測試用例的地方。打開該文件,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
// // TDDDemoTests.swift // TDDDemoTests // // Created by Colin on 16/6/3. // Copyright © 2016年 Colin. All rights reserved. // import XCTest import TDDDemo class TDDDemoTests: XCTestCase { 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() } func testExample() { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct results. } func testPerformanceExample() { // This is an example of a performance test case. self.measureBlock { // Put the code you want to measure the time of here. } } } |
其中,有幾個地方需要說明一下:
1 2 |
import XCTest import TDDDemo |
每一個測試用例都需要引入 XCTest
框架,它定義了我們需要的 XCTestCase
類,以及之后會用到的一些斷言,比如XCTAssertEqual
等。另外,還需要手動導入 TDDDemo
模塊,我們之后的相關代碼都會在 TDDDemo
中編寫,但是默認情況下,類,結構體,枚舉以及它們的方法,都是內聯的(internal
),這意味著它們所處模塊外無法直接訪問到它們。所以在此之外的測試代碼無法訪問到它們,故而需要使用 @testable
關鍵字來讓測試代碼能訪問它們。
再看 setUp
方法和 tearDown
。在每個測試用例調用前,都會先調用 setUp
方法,在每個測試用例執行結束后,都會調用 tearDown
方法,大體流程就是:setUp — test case — tearDown — setUp — test case — tearDown …. 所以我們一般在 setUp
中做一些初始化操作,在 tearDown
做一些清除釋放操作。
另外,每一個測試方法都需要以 test
開頭,這樣 Xcode 才能自動識別出它。比如默認提供的 testExample
和testPerformanceExample
。
再有,這里建議在 Bulid 開始的時候,新建一個導航欄,并且打印 Build Log,這樣我們能更直觀知道發生了什么,哪里出錯了。具體設置如下: Xcode | Preference | Behaviors
如圖所示:
現在 Command + U,執行測試。毋庸置疑,測試通過(畢竟啥都還沒開始寫…)。你會看到如下界面:
左邊的 Test Navigation 列舉了所有的測試用例以及對應的測試結果。中間的編輯區展示了 Bulid 過程中具體做了什么,以及 Build 結果。
哦,對了。還有一處設置也很有用。
Edit Scheme | Test ,可以看到右邊列舉了所有參與測試的用例。當然我們知道,每個用例的測試都是需要時間的,如果想對某個用例單獨測試,或者不想測試某個用例,相應的勾選和去選就可以了。
好了,萬事俱備,是時候展示真正的技術了!
刪除默認的 TDDDemoTests.swift
文件,重新創建一個 CapitalTest.swift
文件。在 TDDDemoTests
分組中,File | New | File | iOS | Source | Unit Test Case Class ,創建一個名為 CapitalTest 并 繼承自 XCTestCase 的類。如圖所示:
刪掉無用的 testExample,testPerformanceExample 方法。
引用 TDDDemo 類。
1
|
import TDDDemo
|
編寫測試用例:
這里我們要做的是實現句子中單詞首字母的大寫轉換,所以只要寫個測試用例驗證首字母是否都是大寫即可。
1 2 3 4 5 6 7 8 9 |
func testMakeHeadline_ReturnsStringWithEachWordStartCapital() { let viewController = ViewController() let string = "this is A test headline" let headline = viewController.makeHeadline(string) XCTAssertEqual(headline, "This Is A Test Headline") } |
很簡單,我們希望有這樣一個函數 makeHeadline
,它接受一個 String 類型的參數,并返回轉換成功的 String 類型的結果。然后利用 XCTAssertEqual
判斷一下,當左右值相同時,它才會通過。
很顯然,這個時候會保持,且測試不通過,因為我們的 makeHeadline
函數根本就不存在,現在就去實現它。
回到 ViewController.swift 中,添加如下方法。
1 2 3 4 |
func makeHeadline(string: String) -> String { return "This Is A Test Headline" } |
Command + U 走一遍,恭喜你,測試走通了。全部顯示綠色的 Build succeeded。(眼尖的朋友可能發現問題了,不過不急,至少目前為止,我們的測試用例已經通過了~)
然后接下去,做的就是重構了。雖然只寫了幾行代碼,但是還是有優化空間的。
我們之前提到過,setUp 方法將在每個 test case 調用前都自動被調用,所以這里可以放一些初始化相關操作。我們這里初始化了一個 ViewController 類型的對象,不出意外的話,在每個測試用例中中需要初始化一個,這無疑是很麻煩的。所以我們可以把 viewController 提出來,當做 CapitalTest 類的一個屬性,然后在 setUp 方法中去初始化它。具體如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
class CapitalTest: XCTestCase { var viewController: ViewController! override func setUp() { super.setUp() viewController = ViewController() } ///////// } |
接下去,我們需要在編寫另外一個測試用例,以保證第一個測試用例并不是偶然的。這也是我們在實際開發中需要做的,列舉多個測試用例,來保證某個功能確實通過了。
1 2 3 4 5 6 7 |
func testMakeHeadline_ReturnsStringWithEachWordStartCapital2() { let string = "Here is another Example" let headline = viewController.makeHeadline(string) XCTAssertEqual(headline, "Here Is Another Example") } |
再次 Command + U,不出意外,第一個還是通過,第二個則顯示失敗。原因大家都懂~
接下去修改 makeHeadline
的具體實現:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
func makeHeadline(string: String) -> String { // 1. 通過“ ”分割字符串, 存入數組 let words = string.componentsSeparatedByString(" ") // 2. 遍歷數組, 移除首字母, 并插入對應的大寫字母 var headline = "" for var word in words { let firstCharacter = word.removeAtIndex(word.startIndex) headline += "\(String(firstCharacter).uppercaseString)\(word) " } // 3. 移除最后的“ ” headline.removeAtIndex(headline.endIndex.predecessor()) return headline } |
代碼很簡單,注釋也寫的很清楚,這里就不累述了。再次 Command + U,bingo~ 通過了。
接下去再看看,是否有優化的空間。
OK,既然不好,那就優化一下唄~
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
func testMakeHeadline_ReturnsStringWithEachWordStartCapital() { let inputString = "this is A test headline" let expectedHeadline = "This Is A Test Headline" let result = viewController.makeHeadline(inputString) XCTAssertEqual(result, expectedHeadline) } func makeHeadline(string: String) -> String { let words = string.componentsSeparatedByString(" ") let headline = words.map { (var word) -> String in let firstCharacter = word.removeAtIndex(word.startIndex) return "\(String(firstCharacter).uppercaseString)\(word)" }.joinWithSeparator(" ") return headline } |
再次 Command + U,確保測試通過。至此,這個簡單的例子算是介紹完了。
雖然例子簡單,只實現了一個功能,但是 TDD 相關的東西,具體流程也都涉及了,剩下的,只是重復這些操作直至完成所有需求。
原文轉自: http://colin1994.github.io/2016/06/03/TDD-With-Swift/