Testes de unidade para o cĂłdigo RxSwift

OlĂĄ Habr! Apresento a vocĂȘ a tradução do artigo "Testing Your RxSwift Code", de Shai Mishali, de raywenderlich.com .


Escrever aplicativos reativos com o RxSwift difere conceitualmente de escrever aplicativos da "maneira usual". Difere no sentido de que os objetos em seu aplicativo geralmente nĂŁo terĂŁo um significado claro; em vez disso, serĂŁo representados por um fluxo de valores no eixo do tempo, conhecido como RxSwift Observable. Este artigo fornece a chave para testar o cĂłdigo RxSwift.


Observable- Um mecanismo poderoso que permite que vocĂȘ, como desenvolvedor, responda Ă s alteraçÔes e verifique se o estado do seu aplicativo estĂĄ sempre atualizado. Juntamente com todos os benefĂ­cios que ele oferece, o teste ObservablenĂŁo Ă© uma tarefa trivial como simplesmente usar o XCTAssert para valores comuns. Mas nĂŁo se preocupe: este artigo o ajudarĂĄ a se tornar um especialista em testar o RxSwift!


Este artigo ensinarĂĄ como criar testes de unidade para Observablethreads. VocĂȘ aprenderĂĄ algumas das tĂ©cnicas disponĂ­veis para testar o cĂłdigo RxSwift, bem como algumas dicas. Vamos começar.


Nota: Este artigo pressupĂ”e que vocĂȘ jĂĄ esteja familiarizado com o RxSwift e como escrever testes simples usando o XCTest.

Começando


Os aplicativos reativos tĂȘm bom desempenho quando vocĂȘ precisa trabalhar com alteraçÔes de conteĂșdo, portanto, trabalharemos exatamente com esse aplicativo.


Faça o download do projeto inicial . VocĂȘ encontrarĂĄ o projeto inicial deste tutorial: Raytronome Ă© um aplicativo de metrĂŽnomo que vocĂȘ pode usar para praticar mĂșsica. Como vocĂȘ pode entender, devido ao fato de os metrĂŽnomos funcionarem ao longo do tempo, vocĂȘ precisa ver muita lĂłgica que pode ser testada.


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 .


Muito mais pode ser aprendido no RxSwift e no RxBlocking - seu trabalho interno, operadores e assim por diante. O melhor lugar para continuar é a documentação oficial do RxSwift , bem como uma lista de operadores do RxBlocking .


Se vocĂȘ tiver alguma dĂșvida ou comentĂĄrio sobre o conteĂșdo do artigo - seja bem-vindo aos comentĂĄrios ou na discussĂŁo do artigo original . Obrigado pela leitura! Atenção , este artigo Ă© uma tradução do artigo de raywenderlich.com .


All Articles