---
# System prepended metadata

title: Chapter 2 - The TDD Cycle
tags: [Bing]

---

# 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
![TDD流程圖](https://i.imgur.com/ZFvZ8xs.png)

## 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週期的事，最後，程式的覆蓋率應該會滿高的。

