RxSwift代码的单元测试

哈Ha!我向您介绍raywenderlich.com上 Shai Mishali 撰写的文章“测试RxSwift代码”翻译


从概念上讲,使用RxSwift编写反应式应用程序与以“常规方式”编写应用程序有所不同。它不同于在这个意义上,在你的应用程序中的对象通常不会有一个明确的意义,相反,他们会通过代表的流值在时间轴上,被称为RxSwift Observable本文将为您提供测试RxSwift代码的关键。


Observable-一种强大的机制,使您作为开发人员能够响应更改,并确保应用程序的状态始终是最新的。加上它提供的所有好处,测试Observable并不是一件简单的任务,不仅仅是简单地将XCTAssert用于普通值。但请放心-本文将指导您成为测试RxSwift的专家!


本文将教您如何为Observable线程创建单元测试您将学习一些测试RxSwift代码的可用技术,以及一些技巧。开始吧。


注意:本文假定您已经熟悉RxSwift以及如何使用XCTest编写简单的测试。

入门


当您需要更改内容时,反应性应用程序的性能很好,因此我们将仅使用此类应用程序。


下载启动程序项目您将找到本教程的入门项目:Raytronome是一个节拍器应用程序,可用于练习音乐。如您所知,由于节拍器会随着时间的推移而工作,因此您必须看到许多可以测试的逻辑。


Raytronome.xcworkspace. Main.storyboard. , .


. Play . (Signature) (Tempo).



UIViewController — MetronomeViewController.swift, MetronomeViewModel.swift -, .


Observable


RxSwift Observable .


, .


; . Observable , , () .



, :


  • ,
  • . ""


, . , MetronomeViewModel, ViewModel - .


MetronomeViewModel.swift. ViewModel , : , ( ), , , , ,



UI. :


  • 4
  • 4/4
  • 120
  • Play/Pause isPlaying
  • , , .
  • "" .
  • .even .odd

RxSwift, RxBlocking RxTest. .


RxBlocking


RaytronomeTests.swift.


; RxSwift, RxCocoa, RxTest RxBlocking, viewModel setUp() MetronomeViewModel


, 4. , , . RxBlocking!


RxBlocking — RxSwift, : Observable BlockingObservable, Observable , .



, — , completed error


RxBlocking , :


  • toArray(): .
  • first(): .
  • last(): .

, first().


RaytronomeTests:


func testNumeratorStartsAt4() throws {
  XCTAssertEqual(try viewModel.numeratorText.toBlocking().first(), "4")
  XCTAssertEqual(try viewModel.numeratorValue.toBlocking().first(), 4)
}

func testDenominatorStartsAt4() throws {
  XCTAssertEqual(try viewModel.denominatorText.toBlocking().first(), "4")
}

toBlocking() BlockingObservable, first() . XCTAssert .


, throws, RxBlocking . throws try! .


Command-U



signatureText 4/4, tempoText 120 BPM.


,


func testSignatureStartsAt4By4() throws {
  XCTAssertEqual(try viewModel.signatureText.toBlocking().first(), "4/4")
}

func testTempoStartsAt120() throws {
  XCTAssertEqual(try viewModel.tempoText.toBlocking().first(), "120 BPM")
}

RxBlocking


, RxBlocking , "" . , :


  1. , , , , RxBlocking . , — — RxBlocking
  2. RxBlocking . Observable , BlockingObservable .
  3. , , RxBlocking , .
  4. RxBlocking , .

. : Play/Pause isPlaying, (tappedPlayPause ). .


RxTest


, RxBlocking , , , .


RxTest!


RxTest RxBlocking, , , . - , — TestScheduler.





— RxSwift, , ,


RxSwift , .


: " ?"


RxTestTestScheduler — . , Observable Observer, "" .


.



TestScheduler. DisposeBag Disposable . viewModel :


var scheduler: TestScheduler!
var disposeBag: DisposeBag!

, setUp(), TestScheduler DisposeBag :


scheduler = TestScheduler(initialClock: 0)
disposeBag = DisposeBag()

TestScheduler initialClock, " " . DisposeBag . .


!


"" Play/Pause , isPlaying .


:


  1. Observable "" tappedPlayPause.
  2. Observer' isPlaying.
  3. .

, !


. RxTest-:


func testTappedPlayPauseChangesIsPlaying() {
  // 1
  let isPlaying = scheduler.createObserver(Bool.self)

  // 2
  viewModel.isPlaying
    .drive(isPlaying)
    .disposed(by: disposeBag)

  // 3
  scheduler.createColdObservable([.next(10, ()),
                                  .next(20, ()),
                                  .next(30, ())])
           .bind(to: viewModel.tappedPlayPause)
           .disposed(by: disposeBag)

  // 4
  scheduler.start()

  // 5
  XCTAssertEqual(isPlaying.events, [
    .next(0, false),
    .next(10, true),
    .next(20, false),
    .next(30, true)
  ])
}

, . :


  1. TestScheduler TestableObserver , Observable — Bool. Observaer — events, .
  2. drive() "" viewModel.isPlaying TestableObserver. "" .
  3. Observable, "" tappedPlayPause. , Observable, TestableObservable, TestScheduler "" .
  4. start() .
  5. XCTAssertEqual RxTest, , isPlaying , . 10, 20 30 , , 0isPlaying.

? : viewModel . , .



Command-u. 5 .




, 0, 10, 20 30 , .


RxTest (Date) , VirtualTimeUnit ( Int).


RxSwiftTestScheduler .


, — , , , 10 10 , . .


, TestScheduler, ?


:


func testModifyingNumeratorUpdatesNumeratorText() {
  let numerator = scheduler.createObserver(String.self)

  viewModel.numeratorText
           .drive(numerator)
           .disposed(by: disposeBag)

  scheduler.createColdObservable([.next(10, 3),
                                  .next(15, 1)])
           .bind(to: viewModel.steppedNumerator)
           .disposed(by: disposeBag)

  scheduler.start()

  XCTAssertEqual(numerator.events, [
    .next(0, "4"),
    .next(10, "3"),
    .next(15, "1")
  ])
}

func testModifyingDenominatorUpdatesNumeratorText() {
  let denominator = scheduler.createObserver(String.self)

  viewModel.denominatorText
           .drive(denominator)
           .disposed(by: disposeBag)

  // Denominator () -  2   `steppedDenominator + 1`.
  // f(1, 2, 3, 4) = 4, 8, 16, 32
  scheduler.createColdObservable([.next(10, 2),
                                  .next(15, 4),
                                  .next(20, 3),
                                  .next(25, 1)])
          .bind(to: viewModel.steppedDenominator)
          .disposed(by: disposeBag)

  scheduler.start()

  XCTAssertEqual(denominator.events, [
    .next(0, "4"),
    .next(10, "8"),
    .next(15, "32"),
    .next(20, "16"),
    .next(25, "4")
  ])
}

func testModifyingTempoUpdatesTempoText() {
  let tempo = scheduler.createObserver(String.self)

  viewModel.tempoText
           .drive(tempo)
           .disposed(by: disposeBag)

  scheduler.createColdObservable([.next(10, 75),
                                  .next(15, 90),
                                  .next(20, 180),
                                  .next(25, 60)])
           .bind(to: viewModel.tempo)
           .disposed(by: disposeBag)

  scheduler.start()

  XCTAssertEqual(tempo.events, [
    .next(0, "120 BPM"),
    .next(10, "75 BPM"),
    .next(15, "90 BPM"),
    .next(20, "180 BPM"),
    .next(25, "60 BPM")
  ])
}

:


  • testModifyingNumeratorUpdatesNumeratorText: , .
  • testModifyingDenominatorUpdatesNumeratorText: , .
  • testModifyingTempoUpdatesTempoText: ,

, , . , 3, 1. , numeratorText "4" ( ), "3", , , "1".


, denominatorText


, BPM


Command-U, 8 . !



, !


. :


func testModifyingSignatureUpdatesSignatureText() {
  // 1
  let signature = scheduler.createObserver(String.self)

  viewModel.signatureText
           .drive(signature)
           .disposed(by: disposeBag)

  // 2
  scheduler.createColdObservable([.next(5, 3),
                                  .next(10, 1),

                                  .next(20, 5),
                                  .next(25, 7),

                                  .next(35, 12),

                                  .next(45, 24),
                                  .next(50, 32)
                                ])
           .bind(to: viewModel.steppedNumerator)
           .disposed(by: disposeBag)

  // Denominator () -  2   `steppedDenominator + 1`.
  // f(1, 2, 3, 4) = 4, 8, 16, 32
  scheduler.createColdObservable([.next(15, 2), // switch to 8ths
                                  .next(30, 3), // switch to 16ths
                                  .next(40, 4)  // switch to 32nds
                                ])
           .bind(to: viewModel.steppedDenominator)
           .disposed(by: disposeBag)

  // 3
  scheduler.start()

  // 4
  XCTAssertEqual(signature.events, [
    .next(0, "4/4"),
    .next(5, "3/4"),
    .next(10, "1/4"),

    .next(15, "1/8"),
    .next(20, "5/8"),
    .next(25, "7/8"),

    .next(30, "7/16"),
    .next(35, "12/16"),

    .next(40, "12/32"),
    .next(45, "24/32"),
    .next(50, "32/32")
  ])
}

! , , . steppedNumerator steppedDenominator , signatureText .


:



. 9 !


, .


:


  1. 4/4
  2. 24/32.
  3. "-" ; 16/16, 8/8, , , 4/4, 24/16, 24/8 24/4 — .

: , ,


:


func testModifyingDenominatorUpdatesNumeratorValueIfExceedsMaximum() {
  // 1
  let numerator = scheduler.createObserver(Double.self)

  viewModel.numeratorValue
           .drive(numerator)
           .disposed(by: disposeBag)

  // 2

  // Denominator () -  2   `steppedDenominator + 1`.
  // f(1, 2, 3, 4) = 4, 8, 16, 32
  scheduler.createColdObservable([
      .next(5, 4), // switch to 32nds
      .next(15, 3), // switch to 16ths
      .next(20, 2), // switch to 8ths
      .next(25, 1)  // switch to 4ths
      ])
      .bind(to: viewModel.steppedDenominator)
      .disposed(by: disposeBag)

  scheduler.createColdObservable([.next(10, 24)])
           .bind(to: viewModel.steppedNumerator)
           .disposed(by: disposeBag)

  // 3
  scheduler.start()

  // 4
  XCTAssertEqual(numerator.events, [
    .next(0, 4), //  4/4
    .next(10, 24), //  24/32
    .next(15, 16), //  16/16
    .next(20, 8), //  8/8
    .next(25, 4) //  4/4
  ])
}

, , ! :


  1. , TestableObserver "" numeratorValue
  2. , . 32, 24 ( ). 24/32. , numeratorValue.
  3. schelduer
  4. numeratorValue


! Command-U:


XCTAssertEqual failed: ("[next(4.0) @ 0, next(24.0) @ 10]") is not equal to ("[next(4.0) @ 0, next(24.0) @ 10, next(16.0) @ 15, next(8.0) @ 20, next(4.0) @ 25]") -

! .


, numeratorValue 24, , 24/16 24/4. :


  • , 4/8.
  • , 7/8.
  • . 4/4, 7/4 — !


, . :]


.


MetronomeViewModel.swift , numeratorValue:


numeratorValue = steppedNumerator
  .distinctUntilChanged()
  .asDriver(onErrorJustReturn: 0)

:


numeratorValue = steppedNumerator
  .distinctUntilChanged()
  .asDriver(onErrorJustReturn: 0)

steppedNumerator , steppedNumerator maxNumerator .


Command-U, 10 . !




viewModel. , 78%. !


: , Edit Scheme... - , , Tests, Options Code Coverage. Gather coverage for some targets Raytronome . Report Navigator


, . — .


, /, , ( )


32. RaytronomeTests.swift :


func testBeatBy32() {
  // 1
  viewModel = MetronomeViewModel(initialMeter: Meter(signature: "4/32"),
                                 autoplay: true,
                                 beatScheduler: scheduler)

  // 2
  let beat = scheduler.createObserver(Beat.self)
  viewModel.beat.asObservable()
    .take(8)
    .bind(to: beat)
    .disposed(by: disposeBag)

  // 3
  scheduler.start()

  XCTAssertEqual(beat.events, [])
}

. - :


  1. viewModel . 4/32, , tappedPlayPause .
    . , viewModel SerialDispatchQueueScheduler , TestScheduler, , .
  2. TestableObserver Beat 8 beat. 8 — , , .
  3. scheduler. , , , — .

Command-U. :


XCTAssertEqual failed: ("[next(first) @ 1, next(regular) @ 2, next(regular) @ 3, next(regular) @ 4, next(first) @ 5, next(regular) @ 6, next(regular) @ 7, next(regular) @ 8, completed @ 8]") is not equal to ("[]") —

, , ? 1 8.


, - , 4/32 4/4. , .


Meter(signature: "4/32") Meter(signature: "4/4") Command-U. - - .


, ! , - , . , ? -, VirtualTimeUnit, .



120 BPM, 4 ( 4/4), 0.5 . 32, 0.0625 .


, — , TestScheduler , VirtualTimeUnit.


resolution . resolutionTestScheduler 1.


0.0625/1 1, 0.5/1 1, .


, resolution, .


viewModel, , :


scheduler = TestScheduler(initialClock: 0, resolution: 0.01)

resolution


resoulution



4/32 viewModel Command-U.


, .


XCTAssertEqual failed: ("[next(first) @ 6, next(regular) @ 12, next(regular) @ 18, next(regular) @ 24, next(first) @ 30, next(regular) @ 36, next(regular) @ 42, next(regular) @ 48, completed @ 48]") is not equal to ("[]") —

6. XCTAssertEqual :


XCTAssertEqual(beat.events, [
  .next(6, .first),
  .next(12, .regular),
  .next(18, .regular),
  .next(24, .regular),
  .next(30, .first),
  .next(36, .regular),
  .next(42, .regular),
  .next(48, .regular),
  .completed(48)
])

Command-U, , . !


- 4/4 .


:


func testBeatBy4() {
  scheduler = TestScheduler(initialClock: 0, resolution: 0.1)

  viewModel = MetronomeViewModel(initialMeter: Meter(signature: "4/4"),
                                 autoplay: true,
                                 beatScheduler: scheduler)

  let beat = scheduler.createObserver(Beat.self)
  viewModel.beat.asObservable()
    .take(8)
    .bind(to: beat)
    .disposed(by: disposeBag)

  scheduler.start()

  XCTAssertEqual(beat.events, [
    .next(5, .first),
    .next(10, .regular),
    .next(15, .regular),
    .next(20, .regular),
    .next(25, .first),
    .next(30, .regular),
    .next(35, .regular),
    .next(40, .regular),
    .completed(40)
  ])
}

— , resolution, 0.1, 4.


Command-U, , 12 !


, , 99.25% MetronomeViewModel, . : beatType.



beatType — , , , beatType .even .odd. . , , :


func testBeatTypeAlternates() {
  scheduler = TestScheduler(initialClock: 0, resolution: 0.1)

  viewModel = MetronomeViewModel(initialMeter: Meter(signature: "4/4"),
                                 autoplay: true,
                                 beatScheduler: scheduler)

  let beatType = scheduler.createObserver(BeatType.self)
  viewModel.beatType.asObservable()
    .take(8)
    .bind(to: beatType)
    .disposed(by: disposeBag)

  scheduler.start()

  XCTAssertEqual(beatType.events, [
    .next(5, .even),
    .next(10, .odd),
    .next(15, .even),
    .next(20, .odd),
    .next(25, .even),
    .next(30, .odd),
    .next(35, .even),
    .next(40, .odd),
    .completed(40)
  ])
}

?


, . , RxSwift. RxBlocking , RxTest .


, Scheduler, TestScheduler .


RxSwiftRxBlocking中都可以学到更多东西 -它们的内部工作,运算符等。最好的选择是官方的RxSwift文档以及RxBlocking运算符列表


如果您对文章的内容有任何疑问或评论-欢迎发表评论或在原始文章的讨论中谢谢阅读!警告本文摘自raywenderlich.com文章


All Articles