Combine 是一個 Swift 的框架,它提供了一個聲明式的方式來處理非同步的事件和數據流。Combine 可以讓開發者用更簡潔和清晰的代碼來實現複雜的功能,例如網絡請求、用戶輸入、定時器等。Combine 和 SwiftUI 是一個完美的搭配,因為 SwiftUI 是基於 Combine 的反應式 UI 框架。SwiftUI 可以自動更新 UI 的狀態,只要 Combine 發出了相應的事件或數據。這樣就可以避免手動管理 UI 的生命週期和數據綁定,提高了開發效率和用戶體驗。
在前面幾天的文章,我們完成了 StockRecord 與相關的測試,接下來要寫的物件則是存放這堆 StockRecord 的清單,讓 UITableView 或 SwiftUI 的 List/LazyVStack 可以依順序將物件取出
step1: StockTradingRecordStore 物件
Combine 的核心概念是 Publisher 和 Subscriber。Publisher 是一個協議,它定義了一個可以發送一系列的值或者完成或失敗的事件的類型。Subscriber 是一個協議,它定義了一個可以接收 Publisher 發送的值或者完成或失敗的事件的類型。Publisher 和 Subscriber 之間可以通過 subscribe 方法來建立連接,形成一個訂閱關係。當 Publisher 發送一個值或者完成或失敗的事件時,它會通知所有的 Subscriber。
import Combine
import Foundation
class StockTradingRecordStore {
var recordPublisher = CurrentValueSubject<[StockTradingRecord], Never>([])
}
<[StockTradingRecord], Never> 表示這個 publisher 送出的物件是 [StockTradingRecord],第二個參數 Never 的位置,表示如果 publisher send failed 時的 faile type,現在這個位置是 Never。Never 表示這個 send 不會 fail。
step2: 開始寫測試
import XCTest
@testable import TwStockTools
final class StockTradingRecordStoreTests: XCTestCase {
override func setUpWithError() throws {}
override func tearDownWithError() throws {}
func testStockTradingRecordStorePublish() {
let sut = StockTradingRecordStore()
let publisherExpectation = expectation(description: "wait for publisher in \(#file)")
var receivedRecords: [StockTradingRecord] = []
let cancellable = sut.recordPublisher
.dropFirst() /// 因為第一個送出來的是空陣列,要等到變化後,也就是第二個,送出來的才是加進去的 record
.sink { records in
receivedRecords = records
publisherExpectation.fulfill()
}
}
}
sut 表示 system under test,在測試程式碼中,常使用 sut 來宣告被測物件。
unit testing 如果要測 async func (eg. api send, 或 data 會在 n 秒後才回傳的狀況),都要下 expecation。如果沒下 expectation,unit testing 不會等待,會直接跑完,為了讓測試正確的反應程式狀況,XCTest 框架提供了 expectation 和 wait,讓 unit testing 可以測試 async func
step3: StockTradingRecordStore 實作加 record 後,用 publisher 進行 send(_)
class StockTradingRecordStore {
var recordPublisher = CurrentValueSubject<[StockTradingRecord], Never>([])
var records: [StockTradingRecord] = [] {
didSet {
/// 當 records 一有變化,就 send
recordPublisher.send(records)
}
}
func add(_ record: StockTradingRecord) {
records.append(record)
}
}
step4: 在測試寫一個 mock Record,加進 sut 後,將 receivedRecords 拿出來進行測
// StockTradingRecordStoreTests.swift
private func getRecord() -> StockTradingRecord {
StockTradingRecord(stockID: "0050", stockName: "元大50", tradingSide: .buy, tradingShares: 1000, tradingAmount: 120000, tradingDateStr: "2023-09-07")
}
func testStockTradingRecordStorePublish() {
let sut = StockTradingRecordStore()
let publisherExpectation = expectation(description: "wait for publisher in \(#file)")
var receivedRecords: [StockTradingRecord] = []
let cancellable = sut.recordPublisher
.dropFirst() /// 因為第一個送出來的是空陣列,要等到變化後,也就是第二個,送出來的才是加進去的 record
.sink { records in
receivedRecords = records
publisherExpectation.fulfill()
}
let record = getRecord()
sut.add(record)
wait(for: [publisherExpectation], timeout: 1)
cancellable.cancel()
// 比對 "0051" 確定 unit testing 會 failed
XCTAssertEqual(receivedRecords.first?.stockID, "0051")
}
step5: 把 XCTAssertEqual 的第二個參數改成 “0050” test pass
確認所有的 test case 都通過,我們可以對 StockTradingRecordStore 的程式碼,抱持著信心