RxSwift_v1.0筆記——16 Testing with RxTest

RxSwift_v1.0筆記——16 Testing with RxTest

100分

以上這是給你的,為了表揚你沒有略過此章節(jié)。研究表明開發(fā)者略過編寫測試用例有兩個原因:

  1. 他們只會寫沒有錯誤的代碼
  2. 編寫測試用例不好玩

如果你只是第一個原因,那么你被錄用了!如果你也同意第二個原因,那么讓我給你介紹一下我的小朋友:RxTest。基于之所以你開始閱讀這本書并很激動的將RxSwift用于你的APP項目中的所有原因,RxTest(和RxBlocking)也會很快讓你對用RxSwift 代碼 編寫測試用例感興趣。它們會提供一個簡潔的API,讓編寫測試用例變得簡單而有趣。

這個章節(jié)將會給你介紹RxTest,稍后是RxBlocking,用來寫測試

本章將向您介紹RxTest以及RxBlocking,通過針對多個RxSwift操作編寫測試,并針對RxSwift產品代碼編寫測試。

開始 300

這個章節(jié)的啟動設計名字叫Testing,它包含一個掌上APP,可以為輸入的16進制顏色代碼提供紅,綠,藍色值和顏色名字(若有)。運行安裝后,打開這個workspace并運行。你可以看到這個APP用 rayWenderlichGreen開始,但是你可以輸入任意16進制顏色代碼并獲得rgb和顏色名字。

這個APP是使用MVVM設計模式組織起來的,你可以在MVVM章節(jié)學習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:這個章節(jié)是假設你很熟悉在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代碼,并且他們有辦法讓你明白熱的和冷的區(qū)別,當談到observables,在RxSwift里更多的考慮的是observables的特點是而不是具體類型。這有點像一點補充的細節(jié),但是它值得你多加關注,因為在RxSwift 的測試內容以外是沒有這么多討論熱的和冷的observable的。

熱observables:

  • 使用資源是否有訂閱者。
  • 產生元素是否有訂閱者。
  • 主要用于狀態(tài)類型,如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)
}

你做了以下內容:

  1. 像所有使用XCTest的tests一樣,方法名必須以test開頭。你建立了一個名叫amb的測試。
  2. 你使用scheduler的 createObserver(_:)方法與String類型的示意創(chuàng)建了一個觀察者

觀察者將記錄它接收到的每個事件的時間戳,就像在RxSwift中的debug操作,但不會打印任何輸出。在Combining Operators章節(jié)你已經學習了amb操作。amb被用在兩個observables之間,哪個observable首先發(fā)射,它就只傳播它發(fā)射的事件。你需要創(chuàng)建兩個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)")
  ])

這個代碼做了:

  1. 使用 scheduler的createHotObservable(_:)創(chuàng)建一個observableA。
  2. 使用next(::)方法在指定的時間(毫秒)添加.next事件到observableA上 ,第二個參數作為值傳遞。
  3. 創(chuàng)建 名為observableB的熱observable
  4. 用規(guī)定的值在指定的時間增加 .next事件到 observableB

要知道amb將只傳播第一個發(fā)射事件的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訪問每個事件的元素?,F在你能斷言這些實際的結果通過增加下面代碼來匹配你期望的結果

XCTAssertEqual(results, ["1)", "2)", "3)"])

點擊函數 testAmb()左側溝槽(gutter)中的鉆石按鈕來執(zhí)行測試。

當測試結束后,你應該看到完成了(又叫(aka)通過)

通常你將創(chuàng)建一個負面測試來補充這個,例如測試接收到的結果與你知道的他們應該不是這個的結果不一致。這章節(jié)完成之前你還有更多的測試要寫,因此要快速測試你的測試是否工作,按以下內容更改斷言:

XCTAssertEqual(results, ["1)", "2)", "No you didn't!"])

再次運行測試確保出現以下錯誤信息:

XCTAssertEqual failed: ("["1)", "2)", "3)"]") is not equal to ("["1)", "2)", "No you didn't!"]")

撤銷上面的改變再運行測試確保它再次通過。

你花了一整章節(jié)來學習過濾操作,為什么不測試一個呢?增加下面的測試到 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])
}

從頭開始:

  1. 創(chuàng)建一個觀察者,時間類型為Int。
  2. 創(chuàng)建一個熱observable,它每秒schedulers一個.next事件,共5秒。
  3. 創(chuàng)建 filterObservable來保存在observable上使用過濾的結果,過濾條件為判斷元素的值小于3。
  4. 在0時開始調度訂閱并分配它到訂閱屬性以便它將在 tearDown()被銷毀。
  5. 啟動scheduler。
  6. 收集結果。
  7. 斷言你期望的結果。

點擊這個測試旁溝槽的鉆石圖標運行測試,你將得到綠勾指示了測試成功。

這些測試已經同步。當你想測試異步操作,你有兩個選擇。你將首先學習容易的一個,使用RxBlocking。

使用RxBlocking 306

RxBlocking是封裝(housed)在RxSwift repo內部的另一個庫,像RxTest一樣,有它自己的pod且必須分開導入。它的主要目的是通過它的 toBlocking(timeout:)方法,轉換一個observable到 BlockingObservable。這樣做會阻塞當前線程,直到observable終止,或者如果指定了一個超時值(默認情況下為零),并且在observable終止之前達到超時,則會引發(fā)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)"])
}

它做了的如下:

  1. 使用默認的服務質量,創(chuàng)建并發(fā)scheduler來運行異步測試
  2. 創(chuàng)建observable來保持在scheduler上,訂閱到兩個字符串的observable的結果。
  3. 對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。這是顯示在章節(jié)開始處的代碼。

接下來初始化的是一個枚舉類型,定義到模型的常見的顏色名。

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)
}

從頭開始:

  1. 綁定文本框的文本(或者一個空的字符串)到視圖模型的hexString輸入observable
  2. 循環(huán)遍歷按鈕出口的集合,綁定tap并轉換按鈕的標題來決定怎樣更新文本框的文字,與文本框是否應該發(fā)送valueChanged控制事件。
  3. 使用視圖模型的color驅動來更新視圖的背景顏色。
  4. 使用視圖模型的rgb驅動來更新rbgTextField的文本。
  5. 使用實體模型的coloName驅動來更新colorNameTextField的文本。

通過預覽app是如何工作的,你現在能夠針對它來寫測試。在TestingTests組內打開TestingViewModel.swift,按如下修改setUP()的實現:

override func setUp() {
  super.setUp()
  viewModel = ViewModel()
  scheduler = ConcurrentDispatchQueueScheduler(qos: .default)
}

這里,你分配app ViewModel類的一個實體給viewModel屬性,用默認服務質量的一個并發(fā)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!
}

你做了以下工作:

  1. 創(chuàng)建一個稍后實現的預期。
  2. 創(chuàng)建 expectedColor等于紅色的預期的測試結果。
  3. 定義結果稍后分配。

這僅僅是起始代碼?,F在將以下代碼添加到測試以訂閱視圖模型的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)
}
  1. 創(chuàng)建一個訂閱到視圖模型的color驅動。注意你略過了第一個元素,因為驅動將在訂閱上重放初始元素。
  2. 分配.next事件元素到result并在expect上調用fulfill()。
  3. 在視圖模型的hexString上增加一個新的值輸入給observable(一個Variable)。
  4. 用1秒來超時等待expectation的完成,并在閉包中為error提供guard
  5. 斷言期望的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)
  }
}
  1. 創(chuàng)建coloObservable來保存訂閱在并發(fā)scheduler上的observable結果。
  2. 在視圖模型的hexString上增加一個新值輸入給observable。
  3. 使用guard來選擇將調用toBlocking()的結果與1秒的超時綁定,如果拋出,捕獲并打印錯誤,然后斷言實際的結果與預期的匹配。

運行測試確保它是成功的。這個測試本質上與前一個相同。你只是不需要那么辛苦。

接下來,添加此代碼以測試視圖模型的rgb驅動為給定的hexString輸入發(fā)出預期的紅色,綠色和藍色值:

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)
}
  1. 創(chuàng)建rgbObservable來保存在scheduler上的訂閱。
  2. 在視圖模型的hexString上增加一個新值輸入給observable。
  3. 檢索在rgbObservable上調用toBlocking的第一個結果,然后斷言每個值與期望的匹配。

01轉換到0255僅僅是為了匹配測試名并讓接下來的事情更加容易。運行這個測試確保它成功通過。

還有一個要測試的驅動程序 將此測試添加到TestingViewModel,來測試視圖模型的colorName驅動為給定的hexString輸入發(fā)出正確的元素:

func testColorNameIsRayWenderlichGreenWhenHexStringIs006636() {
  // 1
  let colorNameObservable =
    viewModel.colorName.asObservable().subscribeOn(scheduler)
  // 2
  viewModel.hexString.value = "#006636"
  // 3
  XCTAssertEqual("rayWenderlichGreen", try!
    colorNameObservable.toBlocking().first()!)
}
  1. 創(chuàng)建observable
  2. 增加測試值。
  3. 斷言實際的結果來匹配期望的結果。

這是我想起了短語”漂洗和重復“,這是一個好的方式。寫測試就是應該簡單。按Command-U運行在項目中的所有測試,所有測試都應該通過。

使用RxText and RxBlocking寫測試是使用RxSWift和RxCocoa寫數據和UI綁定(以及其他)。這章沒有挑戰(zhàn),因為你將在MVVM章中做更多的視圖模型測試。測試真高興!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容