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 , "" . , :
- , , , , RxBlocking . , â â RxBlocking
- RxBlocking .
Observable , BlockingObservable . - , , RxBlocking , .
- 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 .
:
Observable "" tappedPlayPause.Observer' isPlaying.- .
, !
. RxTest-:
func testTappedPlayPauseChangesIsPlaying() {
let isPlaying = scheduler.createObserver(Bool.self)
viewModel.isPlaying
.drive(isPlaying)
.disposed(by: disposeBag)
scheduler.createColdObservable([.next(10, ()),
.next(20, ()),
.next(30, ())])
.bind(to: viewModel.tappedPlayPause)
.disposed(by: disposeBag)
scheduler.start()
XCTAssertEqual(isPlaying.events, [
.next(0, false),
.next(10, true),
.next(20, false),
.next(30, true)
])
}
, . :
TestScheduler TestableObserver , Observable â Bool. Observaer â events, .drive() "" viewModel.isPlaying TestableObserver. "" .Observable, "" tappedPlayPause. , Observable, TestableObservable, TestScheduler "" .start() .- 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)
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() {
let signature = scheduler.createObserver(String.self)
viewModel.signatureText
.drive(signature)
.disposed(by: disposeBag)
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)
scheduler.createColdObservable([.next(15, 2),
.next(30, 3),
.next(40, 4)
])
.bind(to: viewModel.steppedDenominator)
.disposed(by: disposeBag)
scheduler.start()
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 !
, .
:
4/424/32.- "-" ; 16/16, 8/8, , , 4/4, 24/16, 24/8 24/4 â .
: , ,
:
func testModifyingDenominatorUpdatesNumeratorValueIfExceedsMaximum() {
let numerator = scheduler.createObserver(Double.self)
viewModel.numeratorValue
.drive(numerator)
.disposed(by: disposeBag)
scheduler.createColdObservable([
.next(5, 4),
.next(15, 3),
.next(20, 2),
.next(25, 1)
])
.bind(to: viewModel.steppedDenominator)
.disposed(by: disposeBag)
scheduler.createColdObservable([.next(10, 24)])
.bind(to: viewModel.steppedNumerator)
.disposed(by: disposeBag)
scheduler.start()
XCTAssertEqual(numerator.events, [
.next(0, 4),
.next(10, 24),
.next(15, 16),
.next(20, 8),
.next(25, 4)
])
}
, , ! :
- ,
TestableObserver "" numeratorValue - , .
32, 24 ( ). 24/32. , numeratorValue. schelduernumeratorValue

! 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() {
viewModel = MetronomeViewModel(initialMeter: Meter(signature: "4/32"),
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, [])
}
. - :
viewModel . 4/32, , tappedPlayPause .
. , viewModel SerialDispatchQueueScheduler , TestScheduler, , .TestableObserver Beat 8 beat. 8 â , , .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 .