RxSwift_v1.0筆記——16 Testing with RxTest

RxSwift_v1.0筆記——16 Testing with RxTest

100分

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

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

如果你只是第一個原因,那么你被錄用了!如果你也同意第二個原因,那么讓我給你介紹一下我的小朋友: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)
}

你做了以下內容:

  1. 像所有使用XCTest的tests一樣,方法名必須以test開頭。你建立了一個名叫amb的測試。
  2. 你使用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)")
  ])

這個代碼做了:

  1. 使用 scheduler的createHotObservable(_:)創建一個observableA。
  2. 使用next(::)方法在指定的時間(毫秒)添加.next事件到observableA上 ,第二個參數作為值傳遞。
  3. 創建 名為observableB的熱observable
  4. 用規定的值在指定的時間增加 .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])
}

從頭開始:

  1. 創建一個觀察者,時間類型為Int。
  2. 創建一個熱observable,它每秒schedulers一個.next事件,共5秒。
  3. 創建 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終止之前達到超時,則會引發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. 使用默認的服務質量,創建并發scheduler來運行異步測試
  2. 創建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。這是顯示在章節開始處的代碼。

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

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. 循環遍歷按鈕出口的集合,綁定tap并轉換按鈕的標題來決定怎樣更新文本框的文字,與文本框是否應該發送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屬性,用默認服務質量的一個并發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. 創建一個稍后實現的預期。
  2. 創建 expectedColor等于紅色的預期的測試結果。
  3. 定義結果稍后分配。

這僅僅是起始代碼。現在將以下代碼添加到測試以訂閱視圖模型的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. 創建一個訂閱到視圖模型的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. 創建coloObservable來保存訂閱在并發scheduler上的observable結果。
  2. 在視圖模型的hexString上增加一個新值輸入給observable。
  3. 使用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)
}
  1. 創建rgbObservable來保存在scheduler上的訂閱。
  2. 在視圖模型的hexString上增加一個新值輸入給observable。
  3. 檢索在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()!)
}
  1. 創建observable
  2. 增加測試值。
  3. 斷言實際的結果來匹配期望的結果。

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

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,247評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,520評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,362評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,805評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,541評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,896評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,887評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,062評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,608評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,356評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,555評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,077評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,769評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,175評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,489評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,289評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,516評論 2 379

推薦閱讀更多精彩內容