# Chapter 2 - The TDD Cycle ###### tags: `Bing` # Sections 1. Intro TDD Cycle 2. Demo 3. Discussion # TL;DR 1. Red:寫會**錯誤**的test code 2. Green:寫**最短**的app code滿足test 3. 重構:重構你的app code和test code 4. 重複:重複以上流程直到所以features都做完 --- # TDD Cycle  ## Red 先寫一個會**出錯**的test code。 舉例來說,測試『實體化一個ViewModel是否為空?』,但還沒撰寫ViewModel的類別,這必定會出錯,這時就可以進入Green階段。 ## Green 寫一段app code讓你的test code可以順利通過。切記請撰寫最少的程式,不要讓test code進度落後於app code。 舉例來說,撰寫`class ViewModel {}`就該停手了,因為你的測試已經可以通過。 ## Refactor 重構app code和test code,以下提供幾個面向: 1. 重複的邏輯:重複的部分是否可以用propertie, method, class方式分離出來 2. 註解:開發過程中,可能會寫註解協助自身快速進入狀況,像是`// TODO: xxx`或是`// area = 0.5 * a * h`,但在此階段應該以更加優秀的命名來取代這些註解,這部分就要參考Clean Code命名章節。 3. Code smells:聞出你程式的怪味道,並修改,像是用大量if-else,用hardcore string...等。 ## Repeat 重複TDD Cycle,最終,你將得到一個擁有良好測試的APP(...應該吧?🧐🧐🧐) > 💡Tips: ⌘ + U 可以快速執行全部的測試程式 --- # Demo 以下會展示書中範例程式。 1. init 2. init(availableFund) 3. add One Item 4. add Two Item ## 1. init ### Red ```swift= class CashRegisterTests: XCTestCase { override func setUpWithError() throws { super.setUp() } override func tearDownWithError() throws { super.tearDown() } func testInit_createCashRegister() throws { XCTAssertNotNil(CashRegister()) } } // Cannot find 'CashRegister' in scope ``` ### Green ```swift= class CashRegister { } ``` ### Refactor 嗯...先不需要。 ## 2. init(availableFund) ### Red ```swift= func testInitAvailableFunds_setAvailableFunds() { // given let availableFunds = Decimal(100) // when let sut = CashRegister(availableFunds: availableFunds) // then XCTAssertEqual(sut.availableFunds, availableFunds) } // Argument passed to call that takes no arguments ``` 這裡使用given, when, then方式來撰寫測試 * Given a certain condition * When a certain action happens * Then an expected result occurs sut是System Under Test的縮寫,表示本次測試的目標,是在測試常用的命名。 ### Green ```swift= class CashRegister { var availableFunds: Decimal init(availableFunds: Decimal = 0) { self.availableFunds = availableFunds } } ``` ### Refactor 依照需求,`CashRegister.init()`這方法已經不需要了,我們只需要`init(availableFunds: Decimal)`即可,所以,我們把`testInit_createCashRegister()`刪掉。 值得討論的是,availableFunds是否需要預設值,這牽涉設計邏輯,若經過討論是需要預設值,則增加一個`testInit_setsDefaultAvailableFunds`測試案例較為合適。 ```swift= func testInit_setsDefaultAvailableFunds() { let sut = CashRegister() XCTAssertEqual(sut.availableFunds, 0) } ``` 若不需要,則把程式改為: ```swift= init(availableFunds: Decimal) { self.availableFunds = availableFunds } ``` ## 3. add One Item ### Red ```swift= func testAddItem_oneItem_addsCostToTransactionTotal() { // given let availableFunds = Decimal(100) let sut = CashRegister(availableFunds: availableFunds) let itemCost = Decimal(42) // when sut.addItem(itemCost) // then XCTAssertEqual(sut.transactionTotal, itemCost) } // Value of type 'CashRegister' has no member 'addItem' ``` ### Green ```swift= func addItem(_ cost: Decimal) { transactionTotal = cost } ``` > ❗️ Warning: 你可能會想,為什麼是`=`不是`+=`,切記,app code不要做超出你的test code所要做的事情,永遠寫最小的程式完成需求就好。 ### Refactor 可以發現測試程式有部分地方重複,像是availableFunds跟sut在這整個測試當中是重複出現的,可以把他整理一下。 ```swift= var availableFunds: Decimal! var sut: CashRegister! override func setUpWithError() throws { super.setUp() availableFunds = Decimal(100) sut = CashRegister(availableFunds: availableFunds) } override func tearDownWithError() throws { availableFunds = nil sut = nil super.tearDown() } ``` 接著再修正其他測試程式,如此一來,程式就會精簡很多。 > ❗️ Warning: XCTestCase的subclass就是一個Test Case,當在執行全部的測試程式時,系統並不會執行一個Test Case就release一次,而是會將全部的Test Case都執行完畢後才一起release,所以,切記要在`tearDown()`的地方將變數設為nil,以免當Test Case過多時,造成記憶體跟效能的影響。 ## 4. add Two Item ### Red ```swift= func testAddItem_twoItems_addsCostsToTransactionTotal() { // given let itemCost = Decimal(42) let itemCost2 = Decimal(20) let expectedTotal = itemCost + itemCost2 // when sut.addItem(itemCost) sut.addItem(itemCost2) // then XCTAssertEqual(sut.transactionTotal, expectedTotal) } // XCTAssertEqual failed: ("20") is not equal to ("62") ``` ### Green ```swift= func addItem(_ cost: Decimal) { transactionTotal += cost } ``` 可以把`=`改成`+=`了! ### Refactor 測試程式中,itemCost有重複出現過,可以將其整理一下。 ```swift= var itemCost: Decimal! override func setUpWithError() throws { super.setUp() itemCost = Decimal(42) } override func tearDownWithError() throws { itemCost = nil super.tearDown() } ``` 接著再修正其他測試程式,讓程式再更精簡。 `sut.addItem(itemCost)`這段程式雖然出現在`_oneItem_`和`_twoItems_`測試程式裡,但他未出現在`testInitAvailableFund`裡,未來你也可能寫更多的測試,`addItem()`的確不太可能會一直使用到,所以,`addItem()`**不需要**放進`setup()`和`tearDown()`當中。 # Discussion 先講講自己心得,真實世界一定比這個複雜許多,書中提供簡單例子只是讓我們意會,而我在雲裡霧裡悟出一些道理: 1. TDD的規則,讓我們可以以一個high-level角度去開發你的程式,而不會想到什麼就寫點什麼。 2. 『寫最短的程式滿足測試』這點對我來說會有點難適應,但它出發點應該是讓我們關注當下即可,假設現在在寫成功的條件,你就不要去考慮失敗的條件,那應該是另一個TDD週期的事,最後,程式的覆蓋率應該會滿高的。
×
Sign in
Email
Password
Forgot password
or
By clicking below, you agree to our
terms of service
.
Sign in via Facebook
Sign in via Twitter
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
New to HackMD?
Sign up