# 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