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 Observable
no 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 Observable
hilos. 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/4
24/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
. schelduer
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() {
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 .