Hallo Habr! Ich prĂ€sentiere Ihnen die Ăbersetzung des Artikels "Testen Ihres RxSwift-Codes" von Shai Mishali von raywenderlich.com .
Das Schreiben reaktiver Anwendungen mit RxSwift unterscheidet sich konzeptionell vom Schreiben von Anwendungen auf "ĂŒbliche Weise". Dies unterscheidet sich in dem Sinne, dass die Objekte in Ihrer Anwendung normalerweise keine eindeutige Bedeutung haben, sondern durch einen Wertstrom auf der Zeitachse dargestellt werden, der als RxSwift bezeichnet wird Observable
. In diesem Artikel erhalten Sie den SchlĂŒssel zum Testen des RxSwift-Codes.
Observable
- Ein leistungsstarker Mechanismus, mit dem Sie als Entwickler auf Ănderungen reagieren und sicherstellen können, dass der Status Ihrer Anwendung immer auf dem neuesten Stand ist. Zusammen mit all den Vorteilen, die es bietet, ist das Testen Observable
keine so triviale Aufgabe wie die einfache Verwendung von XCTAssert fĂŒr gewöhnliche Werte. Aber keine Sorge - dieser Artikel hilft Ihnen dabei, ein Experte fĂŒr das Testen von RxSwift zu werden!
In diesem Artikel erfahren Sie, wie Sie Komponententests fĂŒr Observable
Threads erstellen . Sie lernen einige der verfĂŒgbaren Techniken zum Testen von RxSwift-Code sowie einige Tipps kennen. Lasst uns beginnen.
Hinweis: In diesem Artikel wird davon ausgegangen, dass Sie bereits mit RxSwift und dem Schreiben einfacher Tests mit XCTest vertraut sind.
Loslegen
Reaktive Anwendungen funktionieren gut, wenn Sie mit sich Ă€ndernden Inhalten arbeiten mĂŒssen. Daher arbeiten wir mit einer solchen Anwendung.
Laden Sie das Starterprojekt herunter . Sie finden das Starterprojekt fĂŒr dieses Tutorial: Raytronome ist eine Metronomanwendung, mit der Sie Musik ĂŒben können. Wie Sie verstehen können, mĂŒssen Sie aufgrund der Tatsache, dass Metronome im Laufe der Zeit arbeiten, eine Menge Logik sehen, die getestet werden kann.
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
.
Sowohl in RxSwift als auch in RxBlocking kann noch viel mehr gelernt werden - ihre interne Arbeit, Operatoren und so weiter. Der beste Ort, um fortzufahren, ist die offizielle RxSwift- Dokumentation sowie eine Liste der RxBlocking- Operatoren .
Wenn Sie Fragen oder Kommentare zum Inhalt des Artikels haben, begrĂŒĂen Sie die Kommentare oder die Diskussion des Originalartikels . Danke fĂŒrs Lesen! Achtung , dieser Artikel ist eine Ăbersetzung des Artikels von raywenderlich.com .