Unit-Tests fĂŒr RxSwift-Code

Hallo Habr! Ich prĂ€sentiere Ihnen die Übersetzung des Artikels "Testen Ihres RxSwift-Codes" von Shai Mishali von raywenderlich.com .


Das Schreiben reaktiver Anwendungen mit RxSwift unterscheidet sich konzeptionell vom Schreiben von Anwendungen auf "ĂŒbliche Weise". Dies unterscheidet sich in dem Sinne, dass die Objekte in Ihrer Anwendung normalerweise keine eindeutige Bedeutung haben, sondern durch einen Wertstrom auf der Zeitachse dargestellt werden, der als RxSwift bezeichnet wird Observable. In diesem Artikel erhalten Sie den SchlĂŒssel zum Testen des RxSwift-Codes.


Observable- Ein leistungsstarker Mechanismus, mit dem Sie als Entwickler auf Änderungen reagieren und sicherstellen können, dass der Status Ihrer Anwendung immer auf dem neuesten Stand ist. Zusammen mit all den Vorteilen, die es bietet, ist das Testen Observablekeine so triviale Aufgabe wie die einfache Verwendung von XCTAssert fĂŒr gewöhnliche Werte. Aber keine Sorge - dieser Artikel hilft Ihnen dabei, ein Experte fĂŒr das Testen von RxSwift zu werden!


In diesem Artikel erfahren Sie, wie Sie Komponententests fĂŒr ObservableThreads erstellen . Sie lernen einige der verfĂŒgbaren Techniken zum Testen von RxSwift-Code sowie einige Tipps kennen. Lasst uns beginnen.


Hinweis: In diesem Artikel wird davon ausgegangen, dass Sie bereits mit RxSwift und dem Schreiben einfacher Tests mit XCTest vertraut sind.

Loslegen


Reaktive Anwendungen funktionieren gut, wenn Sie mit sich Ă€ndernden Inhalten arbeiten mĂŒssen. Daher arbeiten wir mit einer solchen Anwendung.


Laden Sie das Starterprojekt herunter . Sie finden das Starterprojekt fĂŒr dieses Tutorial: Raytronome ist eine Metronomanwendung, mit der Sie Musik ĂŒben können. Wie Sie verstehen können, mĂŒssen Sie aufgrund der Tatsache, dass Metronome im Laufe der Zeit arbeiten, eine Menge Logik sehen, die getestet werden kann.


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 .


Sowohl in RxSwift als auch in RxBlocking kann noch viel mehr gelernt werden - ihre interne Arbeit, Operatoren und so weiter. Der beste Ort, um fortzufahren, ist die offizielle RxSwift- Dokumentation sowie eine Liste der RxBlocking- Operatoren .


Wenn Sie Fragen oder Kommentare zum Inhalt des Artikels haben, begrĂŒĂŸen Sie die Kommentare oder die Diskussion des Originalartikels . Danke fĂŒrs Lesen! Achtung , dieser Artikel ist eine Übersetzung des Artikels von raywenderlich.com .


All Articles