RxSwift_v1.0筆記——16 Testing with RxTest
100分
以上這是給你的,為了表揚你沒有略過此章節。研究表明開發者略過編寫測試用例有兩個原因:
- 他們只會寫沒有錯誤的代碼
- 編寫測試用例不好玩
如果你只是第一個原因,那么你被錄用了!如果你也同意第二個原因,那么讓我給你介紹一下我的小朋友:RxTest。基于之所以你開始閱讀這本書并很激動的將RxSwift用于你的APP項目中的所有原因,RxTest(和RxBlocking)也會很快讓你對用RxSwift 代碼 編寫測試用例感興趣。它們會提供一個簡潔的API,讓編寫測試用例變得簡單而有趣。
這個章節將會給你介紹RxTest,稍后是RxBlocking,用來寫測試
本章將向您介紹RxTest以及RxBlocking,通過針對多個RxSwift操作編寫測試,并針對RxSwift產品代碼編寫測試。
開始 300
這個章節的啟動設計名字叫Testing,它包含一個掌上APP,可以為輸入的16進制顏色代碼提供紅,綠,藍色值和顏色名字(若有)。運行安裝后,打開這個workspace并運行。你可以看到這個APP用 rayWenderlichGreen開始,但是你可以輸入任意16進制顏色代碼并獲得rgb和顏色名字。
這個APP是使用MVVM設計模式組織起來的,你可以在MVVM章節學習MVVM的相關知識。簡單來說就是邏輯代碼被封裝在視圖模型中,視圖控制器用來控制視圖。除了枚舉流行的顏色名稱之外,整個應用程序都運行在這個邏輯上,您將在本章稍后部分中寫出測試:
// Convert hex text to color
color = hexString.asObservable()
.map { hex in
guard hex.characters.count == 7 else { return .clear }
let color = UIColor(hex: hex)
return color
}
.asDriver(onErrorJustReturn: .clear)
// Convert the color to an rgb tuple
rgb = color.asObservable()
.map { color in
var red: CGFloat = 0.0
var green: CGFloat = 0.0
var blue: CGFloat = 0.0
color.getRed(&red, green: &green, blue: &blue, alpha: nil)
let rgb = (Int(red * 255.0), Int(green * 255.0), Int(blue * 255.0))
return rgb
}
.asDriver(onErrorJustReturn: (0, 0, 0))
// Convert the hex text to a matching name
colorName = hexString.asObservable()
.map { hexString in
let hex = String(hexString.characters.dropFirst())
if let color = ColorName(rawValue: hex) {
return "\(color)"
} else {
return "--"
}
}
.asDriver(onErrorJustReturn: "")
在投入這個代碼到testing之前,編寫兩個針對RxSwift操作的測試用例對學習RxTest 是很用幫助的。
Note:這個章節是假設你很熟悉在iOS系統中用XCTest編寫單元測試,如果你是新手,可以找下我們的視頻課程(原失效)https://www.raywenderlich.com/150521/updated-course-ios-unit-ui-testing
用RxTest測試操作 301
Note:因為Swift包管理的問題,原“RxTests”已經重命名為RxTest。因此如果你在野外(out in the wild)看到了“RxTests”,它很可能是指RxTest。
RxTest是RxSwift的獨立庫。 它在RxSwift repo內托管(host),但需要單獨的pod安裝和導入。 RxTest為測試RxSwift代碼提供了許多有用的補充,例如TestScheduler,它是一個虛擬時間scheduler,可以精確控制測試時間線性操作,包括 next(::), completed(::),和 error(::),可以在測試中的指定時間將這些事件添加到observables。 它還添加了冷和熱observables,你可以把它想象成冷熱三明治。不,不是真的。
什么的是熱和冷的observables? 301
RxSwift用了大量的篇幅去簡化你的Rx代碼,并且他們有辦法讓你明白熱的和冷的區別,當談到observables,在RxSwift里更多的考慮的是observables的特點是而不是具體類型。這有點像一點補充的細節,但是它值得你多加關注,因為在RxSwift 的測試內容以外是沒有這么多討論熱的和冷的observable的。
熱observables:
- 使用資源是否有訂閱者。
- 產生元素是否有訂閱者。
- 主要用于狀態類型,如Variable。
冷observables:
- 僅僅在訂閱時消耗資源
- 有訂閱者才產生元素
- 主要使用異步操作,例如網絡。
你稍后寫的單元測試將使用熱observables。 但是,如果您需要使用另一個需求,請了解不同之處。
打開在TestingTests組中的TestingOperators.swift。在類 TestingOperators的頂部定義了兩個屬性:
var scheduler: TestScheduler!
var subscription: Disposable!
scheduler是 TestScheduler的一個實例,你將使用在每個test中,并且 subscription將保持你每個test中的訂閱。改變setUP()的定義:
override func setUp() {
super.setUp()
scheduler = TestScheduler(initialClock: 0)
}
在setUP()方法中,在每個測試用例開始都會調用它。你用TestScheduler (initialClock: 0)初始化一個新的scheduler。它的意思是你希望在測試開始時啟動測試 scheduler。這很快就會變得有意義。
現在改變 tearDown()的定義:
override func tearDown() {
scheduler.scheduleAt(1000) {
self.subscription.dispose()
}
super.tearDown()
}
tearDown()在每個測試完成時調用。在它里面,在1000毫秒后你調度測試訂閱的銷毀。你寫的每個測試將運行至少1秒,因此在1秒后銷毀測試的訂閱是安全的。
現在朋友,是時候寫測試了。在 tearDown()的定義后面增加一個新的test到TestingOperators:
//1
func testAmb() {
//2
let observer = scheduler.createObserver(String.self)
}
你做了以下內容:
- 像所有使用XCTest的tests一樣,方法名必須以test開頭。你建立了一個名叫amb的測試。
- 你使用scheduler的 createObserver(_:)方法與String類型的示意創建了一個觀察者
觀察者將記錄它接收到的每個事件的時間戳,就像在RxSwift中的debug操作,但不會打印任何輸出。在Combining Operators章節你已經學習了amb操作。amb被用在兩個observables之間,哪個observable首先發射,它就只傳播它發射的事件。你需要創建兩個observables。增加下面代碼到test:
//1
let observableA = scheduler.createHotObservable([
// 2
next(100, "a)"),
next(200, "b)"),
next(300, "c)")
])
// 3
let observableB = scheduler.createHotObservable([
// 4
next(90, "1)"),
next(200, "2)"),
next(300, "3)")
])
這個代碼做了:
- 使用 scheduler的createHotObservable(_:)創建一個observableA。
- 使用next(::)方法在指定的時間(毫秒)添加.next事件到observableA上 ,第二個參數作為值傳遞。
- 創建 名為observableB的熱observable
- 用規定的值在指定的時間增加 .next事件到 observableB
要知道amb將只傳播第一個發射事件的observable的事件。你能夠猜到這個這個測試就是為了測這個。
為了測試這個,增加下面的代碼來使用amb操作并分配結果到一個本地常量:
let ambObservable = observableA.amb(observableB)
Option-click在ambObservable上,你將看到它是 Observable<String>類型。
Note:如果你的Xcode又出了毛病(on the fritz),你可能會看到<<error type>>,不要擔心,運行測試時Xcode會識別它。
下一步,你需要告訴scheduler來調度在指定時間的動作。增加下面代碼:
scheduler.scheduleAt(0) {
self.subscription = ambObservable.subscribe(observer)
}
這里你調度了 ambObservable在0時訂閱到observer,并分配訂閱到 subscription屬性。這樣一來,tearDown()將銷毀訂閱。
為了確實地開始(kick off)測試然后確認結果,增加下面代碼:
scheduler.start()
這將啟動虛擬時間調度程序,并且觀察者將收到您通過amb操作指定的.next事件。
現在你能夠收集和分析結果。輸入以下代碼:
let results = observer.events.map {
$0.value.element!
}
在觀察者的事件屬性上你使用map訪問每個事件的元素。現在你能斷言這些實際的結果通過增加下面代碼來匹配你期望的結果
XCTAssertEqual(results, ["1)", "2)", "3)"])
點擊函數 testAmb()左側溝槽(gutter)中的鉆石按鈕來執行測試。
當測試結束后,你應該看到完成了(又叫(aka)通過)
通常你將創建一個負面測試來補充這個,例如測試接收到的結果與你知道的他們應該不是這個的結果不一致。這章節完成之前你還有更多的測試要寫,因此要快速測試你的測試是否工作,按以下內容更改斷言:
XCTAssertEqual(results, ["1)", "2)", "No you didn't!"])
再次運行測試確保出現以下錯誤信息:
XCTAssertEqual failed: ("["1)", "2)", "3)"]") is not equal to ("["1)", "2)", "No you didn't!"]")
撤銷上面的改變再運行測試確保它再次通過。
你花了一整章節來學習過濾操作,為什么不測試一個呢?增加下面的測試到 TestingOperators,它與 testAmb()保持了一樣的格式:
func testFilter() {
// 1
let observer = scheduler.createObserver(Int.self)
// 2
let observable = scheduler.createHotObservable([
next(100, 1),
next(200, 2),
next(300, 3),
next(400, 2),
next(500, 1)
])
// 3
let filterObservable = observable.filter {
$0 < 3
}
// 4
scheduler.scheduleAt(0) {
self.subscription = filterObservable.subscribe(observer)
}
// 5
scheduler.start()
// 6
let results = observer.events.map {
$0.value.element!
}
// 7
XCTAssertEqual(results, [1, 2, 2, 1])
}
從頭開始:
- 創建一個觀察者,時間類型為Int。
- 創建一個熱observable,它每秒schedulers一個.next事件,共5秒。
- 創建 filterObservable來保存在observable上使用過濾的結果,過濾條件為判斷元素的值小于3。
- 在0時開始調度訂閱并分配它到訂閱屬性以便它將在 tearDown()被銷毀。
- 啟動scheduler。
- 收集結果。
- 斷言你期望的結果。
點擊這個測試旁溝槽的鉆石圖標運行測試,你將得到綠勾指示了測試成功。
這些測試已經同步。當你想測試異步操作,你有兩個選擇。你將首先學習容易的一個,使用RxBlocking。
使用RxBlocking 306
RxBlocking是封裝(housed)在RxSwift repo內部的另一個庫,像RxTest一樣,有它自己的pod且必須分開導入。它的主要目的是通過它的 toBlocking(timeout:)方法,轉換一個observable到 BlockingObservable。這樣做會阻塞當前線程,直到observable終止,或者如果指定了一個超時值(默認情況下為零),并且在observable終止之前達到超時,則會引發RxError.timeout錯誤。 這基本上將異步操作轉換為同步操作,使測試變得更加容易。
增加下面在RxBlocking內的三行測試代碼到 TestingOperators來測試 toArray操作:
func testToArray() {
// 1
let scheduler = ConcurrentDispatchQueueScheduler(qos: .default)
// 2
let toArrayObservable = Observable.of("1)",
"2)").subscribeOn(scheduler)
// 3
XCTAssertEqual(try! toArrayObservable.toBlocking().toArray(), ["1)",
"2)"])
}
它做了的如下:
- 使用默認的服務質量,創建并發scheduler來運行異步測試
- 創建observable來保持在scheduler上,訂閱到兩個字符串的observable的結果。
- 對toArrayObservable調用toBlocking()的結果使用toArray,并斷言toArray的返回值等于預期結果。
toBlocking()轉換 toArrayObservable為一個阻塞observable,阻止由scheduler產生的線程,直到它終止。運行測試你應該看到成功。僅用三行代碼就測試了一個異步操作——哇!你將用簡潔的RxBlocking做更多工作,但現在是時候離開操作的測試并寫一些針對(against)應用產品代碼的測試。
測試RxSwift的產品代碼 307
首先打開在Testing組中的ViewModel.swift。在頂部,你將看到一些屬性定義:
let hexString = Variable<String>("")
let color: Driver<UIColor>
let rgb: Driver<(Int, Int, Int)>
let colorName: Driver<String>
hexString接收來至視圖控制器的輸入。color,rgb和colorName是輸出,視圖控制器將綁定到視圖。在視圖模型的初始中,通過轉換另一個observable并把返回結果作為Driver。這是顯示在章節開始處的代碼。
接下來初始化的是一個枚舉類型,定義到模型的常見的顏色名。
enum ColorName: String {
case aliceBlue = "F0F8FF"
case antiqueWhite = "FAEBD7"
case aqua = "0080FF"
// And many more...
現在打開ViewController.swift,聚焦到 viewDidLoad()的實現上。
override func viewDidLoad() {
super.viewDidLoad()
configureUI()
guard let textField = self.hexTextField else { return }
textField.rx.text.orEmpty
.bindTo(viewModel.hexString)
.disposed(by: disposeBag)
for button in buttons {
button.rx.tap
.bindNext {
var shouldUpdate = false
switch button.titleLabel!.text! {
case "?":
textField.text = "#"
shouldUpdate = true
case "←" where textField.text!.characters.count > 1:
textField.text = String(textField.text!.characters.dropLast())
shouldUpdate = true
case "←":
break
case _ where textField.text!.characters.count < 7:
textField.text!.append(button.titleLabel!.text!)
shouldUpdate = true
default:
break
}
if shouldUpdate {
textField.sendActions(for: .valueChanged)
}
}
.disposed(by: self.disposeBag)
}
viewModel.color
.drive(onNext: { [unowned self] color in
UIView.animate(withDuration: 0.2) {
self.view.backgroundColor = color
}
})
.disposed(by: disposeBag)
viewModel.rgb
.map { "\($0.0), \($0.1), \($0.2)" }
.drive(rgbTextField.rx.text)
.disposed(by: disposeBag)
viewModel.colorName
.drive(colorNameTextField.rx.text)
.disposed(by: disposeBag)
}
從頭開始:
- 綁定文本框的文本(或者一個空的字符串)到視圖模型的hexString輸入observable
- 循環遍歷按鈕出口的集合,綁定tap并轉換按鈕的標題來決定怎樣更新文本框的文字,與文本框是否應該發送valueChanged控制事件。
- 使用視圖模型的color驅動來更新視圖的背景顏色。
- 使用視圖模型的rgb驅動來更新rbgTextField的文本。
- 使用實體模型的coloName驅動來更新colorNameTextField的文本。
通過預覽app是如何工作的,你現在能夠針對它來寫測試。在TestingTests組內打開TestingViewModel.swift,按如下修改setUP()的實現:
override func setUp() {
super.setUp()
viewModel = ViewModel()
scheduler = ConcurrentDispatchQueueScheduler(qos: .default)
}
這里,你分配app ViewModel類的一個實體給viewModel屬性,用默認服務質量的一個并發scheduler給scheduler屬性。
現在你可以開始針對app的視圖模型來寫測試了。首先,你將使用傳統的XCTest API編寫一個異步測試。增加視圖模型顏色驅動(使用傳統方式)的測試到TestingViewModel:
func testColorIsRedWhenHexStringIsFF0000_async() {
let disposeBag = DisposeBag()
// 1
let expect = expectation(description: #function)
// 2
let expectedColor = UIColor(red: 1.0, green: 0.0, blue: 0.0, alpha:
1.0)
// 3
var result: UIColor!
}
你做了以下工作:
- 創建一個稍后實現的預期。
- 創建 expectedColor等于紅色的預期的測試結果。
- 定義結果稍后分配。
這僅僅是起始代碼。現在將以下代碼添加到測試以訂閱視圖模型的color驅動程序:
// 1
viewModel.color.asObservable()
.skip(1)
.subscribe(onNext: {
// 2
result = $0
expect.fulfill()
})
.disposed(by: disposeBag)
// 3
viewModel.hexString.value = "#ff0000"
// 4
waitForExpectations(timeout: 1.0) { error in
guard error == nil else {
XCTFail(error!.localizedDescription)
return
}
// 5
XCTAssertEqual(expectedColor, result)
}
- 創建一個訂閱到視圖模型的color驅動。注意你略過了第一個元素,因為驅動將在訂閱上重放初始元素。
- 分配.next事件元素到result并在expect上調用fulfill()。
- 在視圖模型的hexString上增加一個新的值輸入給observable(一個Variable)。
- 用1秒來超時等待expectation的完成,并在閉包中為error提供guard
- 斷言期望的color等于實際的result。
很簡單但有點冗長。運行測試確保它通過。
現在使用RxBlocking來實現同樣的事情:
func testColorIsRedWhenHexStringIsFF0000() {
// 1
let colorObservable =
viewModel.color.asObservable().subscribeOn(scheduler)
// 2
viewModel.hexString.value = "#ff0000"
// 3
do {
guard let result = try colorObservable.toBlocking(timeout:
1.0).first() else { return }
XCTAssertEqual(result, .red)
} catch {
print(error)
}
}
- 創建coloObservable來保存訂閱在并發scheduler上的observable結果。
- 在視圖模型的hexString上增加一個新值輸入給observable。
- 使用guard來選擇將調用toBlocking()的結果與1秒的超時綁定,如果拋出,捕獲并打印錯誤,然后斷言實際的結果與預期的匹配。
運行測試確保它是成功的。這個測試本質上與前一個相同。你只是不需要那么辛苦。
接下來,添加此代碼以測試視圖模型的rgb驅動為給定的hexString輸入發出預期的紅色,綠色和藍色值:
func testRgbIs010WhenHexStringIs00FF00() {
// 1
let rgbObservable =
viewModel.rgb.asObservable().subscribeOn(scheduler)
// 2
viewModel.hexString.value = "#00ff00"
// 3
let result = try! rgbObservable.toBlocking().first()!
XCTAssertEqual(0 * 255, result.0)
XCTAssertEqual(1 * 255, result.1)
XCTAssertEqual(0 * 255, result.2)
}
- 創建rgbObservable來保存在scheduler上的訂閱。
- 在視圖模型的hexString上增加一個新值輸入給observable。
- 檢索在rgbObservable上調用toBlocking的第一個結果,然后斷言每個值與期望的匹配。
01轉換到0255僅僅是為了匹配測試名并讓接下來的事情更加容易。運行這個測試確保它成功通過。
還有一個要測試的驅動程序 將此測試添加到TestingViewModel,來測試視圖模型的colorName驅動為給定的hexString輸入發出正確的元素:
func testColorNameIsRayWenderlichGreenWhenHexStringIs006636() {
// 1
let colorNameObservable =
viewModel.colorName.asObservable().subscribeOn(scheduler)
// 2
viewModel.hexString.value = "#006636"
// 3
XCTAssertEqual("rayWenderlichGreen", try!
colorNameObservable.toBlocking().first()!)
}
- 創建observable
- 增加測試值。
- 斷言實際的結果來匹配期望的結果。
這是我想起了短語”漂洗和重復“,這是一個好的方式。寫測試就是應該簡單。按Command-U運行在項目中的所有測試,所有測試都應該通過。
使用RxText and RxBlocking寫測試是使用RxSWift和RxCocoa寫數據和UI綁定(以及其他)。這章沒有挑戰,因為你將在MVVM章中做更多的視圖模型測試。測試真高興!