Bonjour, Habr! Je vous présente la traduction de l'article "Tester votre code RxSwift" par Shai Mishali de raywenderlich.com .
L'écriture d'applications réactives avec RxSwift diffÚre conceptuellement de l'écriture d'applications de la «maniÚre habituelle». Il diffÚre en ce sens que les objets de votre application n'auront généralement pas de signification claire, mais seront représentés par un flux de valeurs sur l'axe du temps, connu sous le nom de RxSwift Observable. Cet article vous donnera la clé pour tester le code RxSwift.
Observable- Un mĂ©canisme puissant qui vous permet, en tant que dĂ©veloppeur, de rĂ©pondre aux changements et d'ĂȘtre sĂ»r que l'Ă©tat de votre application est toujours Ă jour. Avec tous les avantages qu'il offre, le test Observablen'est pas une tĂąche aussi triviale que l'utilisation simple de XCTAssert pour les valeurs ordinaires. Mais ne vous inquiĂ©tez pas - cet article vous guidera pour devenir un expert en test de RxSwift!
Cet article vous apprendra à créer des tests unitaires pour les Observablethreads. Vous apprendrez certaines des techniques disponibles pour tester le code RxSwift, ainsi que quelques conseils. Commençons.
Remarque: cet article suppose que vous ĂȘtes dĂ©jĂ familiarisĂ© avec RxSwift et comment Ă©crire des tests simples Ă l'aide de XCTest.
Commencer
Les applications réactives fonctionnent bien lorsque vous devez travailler avec un contenu changeant, nous allons donc travailler avec une telle application.
TĂ©lĂ©chargez le projet de dĂ©marrage . Vous trouverez le projet de dĂ©marrage de ce tutoriel: Raytronome est une application de mĂ©tronome que vous pouvez utiliser pour pratiquer la musique. Comme vous pouvez le comprendre, du fait que les mĂ©tronomes fonctionnent dans le temps, vous devez voir beaucoup de logique qui peut ĂȘtre testĂ©e.
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 .
Il y a beaucoup plus à apprendre à la fois dans RxSwift et dans RxBlocking - leur travail interne, leurs opérateurs, etc. Le meilleur endroit pour continuer est la documentation officielle de RxSwift , ainsi qu'une liste des opérateurs RxBlocking .
Si vous avez des questions ou des commentaires sur le contenu de l'article, bienvenue dans les commentaires ou dans la discussion de l' article d'origine . Merci d'avoir lu! Attention , cet article est une traduction de l' article de raywenderlich.com .