# Chepter 5: Test Expectation
###### tags: `Bing`
## TL;DR
1. General test expectations
2. KVO expectations
3. Notification expectations
## Using Expectation

### 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