---
# System prepended metadata

title: 'Chepter 5: Test Expectation'
tags: [Bing]

---

# Chepter 5: Test Expectation

###### tags: `Bing`

## TL;DR
1. General test expectations
2. KVO expectations
3. Notification expectations

## Using Expectation
![](https://i.imgur.com/M8lUEOO.png)

### Async test
測試狀態改變（StateChanged）
流程：狀態改變 -> 觸發 didSet -> 觸發 callback
**🔴 Red**
```swift=
func testAppModel_whenStateChanges_executesCallback() {
    // given
    givenInProgress()
    var observedState = AppState.notStarted
    // 1
    let expected = expectation(description: "callback happend") // identify a failure in the test logs.
    sut.stateChangedCallback = { model in
      observedState = model.appState
      // 2
      expected.fulfill() // is called on the expectation to indicate it has been fulfilled
    }

    // when
    sut.pause()

    // then
    // 3
    wait(for: [expected], timeout: 1) // let test runner to pause until all expectations are fulfilled or the timeout(in seconds) passes.
    XCTAssertEqual(observedState, .paused)
}
```
1. `expectation(description:)` 是 XCTestCase 的方法，會建立一個 XCTestExpectation 物件，`description` 用於失敗時要顯示的 log。
2. `fulfill()` 表示 expectation 被滿足，通常是放在 callback 發生的時候。
3. `wait(for:timeout:)` 會暫停 test runner，直到“所有”的expectation都達成或是 timeout time。

**🟢 Green**
```swift=
private(set) var appState: AppState = .notStarted {
  didSet {
    stateChangedCallback?(self)
  }
}

var stateChangedCallback: ((AppModel) -> ())?
```
**🔆 Notice**
```swift=
// tearDown
sut.stateChangedCallback = nil
```

### KVO (Key-Value Observing)
> Key-Value Observing （KVO）是一種 Cocoa 程式設計模式，用於通知物件關於其他物件屬性屬性的更改。它對於在應用程序中邏輯分離部份間溝通更改非常有用，像是 Model 與 View 之間。你只能使用從 NSObject 繼承的類別使用 Key-Value Observing。 [Swift. Key-Value Observing](https://medium.com/jeremy-xue-s-blog/swift-key-value-observing-5d7561767d60)


流程：stateChangedCallback -> update UI -> update Button -> button.setTitle

**🔴 Red**
```swift=
// StepCountControllerTests.swift
func testController_whenCaught_buttonLabelIsTryAgain() {
    // given
    givenInProgress()
    let exp = expectation(description: "button title change")
    // 1
    let observer = ButtonObserver()
    observer.observe(sut.startButton, expectation: exp)

    // when
    whenCaught()

    // then
    wait(for: [exp], timeout: 1)
    let text = sut.startButton.title(for: .normal)
    XCTAssertEqual(text, AppState.caught.nextStateButtonLabel)
}

func testController_whenComplete_buttonLabelIsStartOver() {
    // given
    givenInProgress()
    // 2
    let exp = keyValueObservingExpectation(for: sut.startButton as Any, keyPath: "titleLabel.text")

    // when
    whenCompleted()

    // then
    wait(for: [exp], timeout: 1)
    let text = sut.startButton.title(for: .normal)
    XCTAssertEqual(text, AppState.completed.nextStateButtonLabel)
}

func whenCaught() {
     AppModel.instance.setToCaught()
}

func whenCompleted() {
     AppModel.instance.setToComplete()
}
```
1. 使用 ButtonObserver 來觀察 startButton 的 label
2. 使用 keyValueObservingExpectation 來觀察 startButton 的 label，跟 (1) 效果一樣，但更簡潔

```swift=
class ButtonObserver: NSObject {
  var expectation: XCTestExpectation?
  weak var button: UIButton?
  
  func obserrve(_ button: UIButton, expectation: XCTestExpectation) {
    self.expectation = expectation
    self.button = button
    
    button.addObserver(self, forKeyPath: "titleLabel.text", options: [.new], context: nil)
  }
  
  override func observeValue(
    forKeyPath keyPath: String?,
    of object: Any?,
    change: [NSKeyValueChangeKey : Any]?,
    context: UnsafeMutableRawPointer?) {
    
    expectation?.fulfill()
  }
  
  deinit {
    button?.removeObserver(self, forKeyPath: "titleLabel.text")
  }
}
```

**🟢 Green**
```swift=
// StepCountController.swift - viewDidLoad
AppModel.instance.stateChangedCallback = { model in
    DispatchQueue.main.async {
        self.updateUI()
    }
}
```

## Wait for Notification 
`(forNotification:object:handler)` 產生一個  expectation，當收到 notification post 後，即觸發 fulfill()。

**🔴 Red**
```swift=
// AlertCenterTests.swift
func testPostOne_generatesANotification() {
  // given
  let exp = expectation(forNotification: AlertNotification.name, object: sut, handler: nil)
  let alert = Alert("this is an alert")
  
  // when
  sut.postAlert(alert: alert)
  
  // then
  wait(for: [exp], timeout: 1)
}
```

**🟢 Green**
```swift=
// AlertCenter.swift
func postAlert(alert: Alert) {
  //stub implementation
  let notification = Notification(name: AlertNotification.name, object: self)
  notificationCenter.post(notification)
}
```

## Waiting for multiple events
如果有多個 Alert ...
`expectedFulfillmentCount` 表示要在 timeout 前發 n 次 fulfill() 才算成功。
```swift=
func testPostTwoAlerts_generatesANotification() {
  // given
  let exp = expectation(forNotification: AlertNotification.name, object: sut, handler: nil)
  exp.expectedFulfillmentCount = 2
  
  let alert1 = Alert("this is the first alert")
  let alert2 = Alert("this is the seconed alert")
  
  // when
  sut.postAlert(alert: alert1)
  sut.postAlert(alert: alert2)
  
  // then
  wait(for: [exp], timeout: 1)
```
**🔆 Notice**
不要寫兩個expectation！如果這樣寫在第一個`postAlert`就會同時滿足兩個exp。

## Expecting something not to happen
一個好的測試不僅僅是要測試正確的情境，也要注意 side effect，比如“同一個 alert”被發送多次，不應該像是垃圾訊息一直彈出

`exp.isInverted = true`表示與預期的結果相反

**🔴 Red**
```swift=
func testPostDouble_generatesANotification() {
  // given
  let exp = expectation(forNotification: AlertNotification.name, object: sut, handler: nil)
  exp.expectedFulfillmentCount = 2
  exp.isInverted = true
  
  let alert = Alert("this is the alert")
  
  // when
  sut.postAlert(alert: alert)
  sut.postAlert(alert: alert)
  
  // then
  wait(for: [exp], timeout: 1)
```
**🟢 Green**
```swift=
private var alertQueue: [Alert] = []

func postAlert(alert: Alert) {
  //stub implementation
  guard !alertQueue.contains(alert) else {
    return
  }
  
  alertQueue.append(alert)
  
  let notification = Notification(name: AlertNotification.name, object: self)
  notificationCenter.post(notification)
}
```

## Testing for multiple expectations
依序完成 expectation
**🔴 Red**
```swift=
func testWhenGoalReached_allMilstoneNotificationsSent() {
  // given
  sut.goal = 400
  let expectations = [
    givenExpectationForNotification(alert: .milestone25Percent),
    givenExpectationForNotification(alert: .milestone50Percent),
    givenExpectationForNotification(alert: .milestone75Percent),
    givenExpectationForNotification(alert: .goalComplete)
  ]
  
  // when
  sut.steps = 400
  
  // then
  wait(for: expectations, timeout: 1, enforceOrder: true)
}
```
`enforceOrder` 可以確保順序正確，可以用來測試需呼叫多個 API 的情境，像是 OAuth 網路登錄。

**🟢 Green**
```swift=
// DataModel.swift
// MARK: - Updates due to distance
private func checkThreshold(percent: Double, alert: Alert) {
  guard !sentAlerts.contains(alert),
    let goal = goal else {
      return
  }
  if Double(steps) >= Double(goal) * percent  {
    AlertCenter.instance.postAlert(alert: alert)
    sentAlerts.append(alert)
  }
}

func updateForSteps() {
  checkThreshold(percent: 0.25, alert: .milestone25Percent)
  checkThreshold(percent: 0.50, alert: .milestone50Percent)
  checkThreshold(percent: 0.75, alert: .milestone75Percent)
  checkThreshold(percent: 1.00, alert: .goalComplete)
}
```

# Key Points
1. 本篇都在講如何測試 async test
2. XCTestExpectation 一般用法
3. keyValueObservingExpectation 測試 KVO
4. XCTestExpectation 用於 notification
5. 依序測試 XCTestExpectation