Pruebas unitarias para el código RxSwift

Hola Habr! Le presento la traducción del artículo "Probar su código RxSwift" por Shai Mishali de raywenderlich.com .


Escribir aplicaciones reactivas con RxSwift conceptualmente difiere de escribir aplicaciones en la "forma habitual". Difiere en el sentido de que los objetos en su aplicación generalmente no tendrán un significado claro, sino que estarán representados por un flujo de valores en el eje de tiempo, conocido como RxSwift Observable. Este artículo le dará la clave para probar el código RxSwift.


Observable- Un poderoso mecanismo que le permite, como desarrollador, responder a los cambios y asegurarse de que el estado de su aplicación esté siempre actualizado. Junto con todos los beneficios que esto brinda, la prueba Observableno es una tarea tan trivial como simplemente usar XCTAssert para valores ordinarios. Pero no se preocupe: ¡este artículo lo guiará para convertirse en un experto en probar RxSwift!


Este artículo le enseñará cómo crear pruebas unitarias para Observablehilos. Aprenderá algunas de las técnicas disponibles para probar el código RxSwift, así como algunos consejos. Empecemos.


Nota: Este artículo asume que ya está familiarizado con RxSwift y cómo escribir pruebas simples con XCTest.

Empezando


Las aplicaciones reactivas funcionan bien cuando necesita trabajar con contenido cambiante, por lo que trabajaremos con tal aplicación.


Descargue el proyecto inicial . Encontrará el proyecto inicial para este tutorial: Raytronome es una aplicación de metrónomo que puede usar para practicar música. Como puedes entender, debido al hecho de que los metrónomos funcionan con el tiempo, tienes que ver mucha lógica que se puede probar.


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 .


Hay mucho más que se puede aprender tanto en RxSwift como en RxBlocking : su trabajo interno, operadores, etc. El mejor lugar para continuar es la documentación oficial de RxSwift , así como una lista de operadores de RxBlocking .


Si tiene alguna pregunta o comentario sobre el contenido del artículo, bienvenido a los comentarios, o en la discusión del artículo original . ¡Gracias por leer! Advertencia , este artículo es una traducción del artículo de raywenderlich.com .


All Articles