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