Unit tests for RxSwift code

Hello, Habr! I present to you the translation of the article "Testing Your RxSwift Code" by Shai Mishali from raywenderlich.com .


Writing reactive applications with RxSwift conceptually differs from writing applications in the "usual way." It differs in the sense that objects in your application will usually not have a clear meaning, instead they will be represented by a stream of values ​​on the time axis, known as RxSwift Observable. This article will give you the key to testing RxSwift code.


Observable- A powerful mechanism that allows you, as a developer, to respond to changes and be sure that the state of your application is always up to date. Together with all the benefits this gives, testing Observableis not such a trivial task as simply using XCTAssert for ordinary values. But don’t worry - this article will guide you on becoming an expert in testing RxSwift!


This article will teach you how to create unit tests for Observablethreads. You will learn some of the available techniques for testing RxSwift code, as well as some tips. Let's start.


Note: This article assumes that you are already familiar with both RxSwift and how to write simple tests using XCTest.

Getting started


Reactive applications perform well when you need to work with changing content, so we will work with just such an application.


Download the starter project . You will find the starter project for this tutorial: Raytronome is a metronome application that you could use to practice music. As you can understand, due to the fact that metronomes work over time, you have to see a lot of logic that can be tested.


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 , .


: " ?"


RxTest β€” TestScheduler β€” . , 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 , , 0 β€” isPlaying.

? : viewModel . , .



Command-u. 5 .




, 0, 10, 20 30 , .


RxTest (Date) , VirtualTimeUnit ( Int).


RxSwift β€” TestScheduler .


, β€” , , , 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 . resolution β€” TestScheduler 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 .


There is much more that can be learned both in RxSwift and in RxBlocking - their internal work, operators and so on. The best place to continue is the official RxSwift documentation , as well as a list of RxBlocking operators .


If you have any questions, or comments on the content of the article - welcome to the comments, or in the discussion of the original article . Thanks for reading! Warning , this article is a translation of the article from raywenderlich.com .


All Articles