Halo, Habr! Saya mempersembahkan kepada Anda terjemahan artikel "Menguji Kode RxSwift Anda" oleh Shai Mishali dari raywenderlich.com .
Menulis aplikasi reaktif dengan RxSwift secara konseptual berbeda dari menulis aplikasi dengan cara "biasa". Ini berbeda dalam arti bahwa objek dalam aplikasi Anda biasanya tidak memiliki makna yang jelas, sebaliknya mereka akan diwakili oleh aliran nilai pada sumbu waktu, yang dikenal sebagai RxSwift Observable
. Artikel ini akan memberi Anda kunci untuk menguji kode RxSwift.
Observable
- Mekanisme kuat yang memungkinkan Anda, sebagai pengembang, untuk merespons perubahan dan memastikan bahwa keadaan aplikasi Anda selalu terkini. Bersama dengan semua manfaat yang diberikan ini, pengujian Observable
bukanlah tugas sepele seperti hanya menggunakan XCTAssert untuk nilai-nilai biasa. Tapi jangan khawatir - artikel ini akan memandu Anda menjadi ahli dalam menguji RxSwift!
Artikel ini akan mengajarkan Anda cara membuat unit test untuk Observable
utas. Anda akan mempelajari beberapa teknik yang tersedia untuk menguji kode RxSwift, serta beberapa tips. Ayo mulai.
Catatan: Artikel ini mengasumsikan bahwa Anda sudah terbiasa dengan RxSwift dan cara menulis tes sederhana menggunakan XCTest.
Mulai
Aplikasi reaktif berfungsi dengan baik ketika Anda perlu bekerja dengan mengubah konten, jadi kami akan bekerja dengan aplikasi seperti itu.
Unduh proyek pemula . Anda akan menemukan proyek awal untuk tutorial ini: Raytronome adalah aplikasi metronom yang dapat Anda gunakan untuk berlatih musik. Seperti yang dapat Anda pahami, karena fakta bahwa metronom bekerja seiring waktu, Anda harus melihat banyak logika yang dapat diuji.
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
.
Ada banyak lagi yang dapat dipelajari baik di RxSwift dan di RxBlocking - pekerjaan internal mereka, operator dan sebagainya. Tempat terbaik untuk melanjutkan adalah dokumentasi RxSwift resmi , serta daftar operator RxBlocking .
Jika Anda memiliki pertanyaan, atau komentar pada konten artikel - selamat datang di komentar, atau dalam diskusi artikel asli . Terima kasih sudah membaca! Peringatan , artikel ini adalah terjemahan dari artikel dari raywenderlich.com .