哈Ha!我向您介绍raywenderlich.com上 Shai Mishali 撰写的文章“测试RxSwift代码”的翻译。
从概念上讲,使用RxSwift编写反应式应用程序与以“常规方式”编写应用程序有所不同。它不同于在这个意义上,在你的应用程序中的对象通常不会有一个明确的意义,相反,他们会通过代表的流值在时间轴上,被称为RxSwift Observable。本文将为您提供测试RxSwift代码的关键。
Observable-一种强大的机制,使您作为开发人员能够响应更改,并确保应用程序的状态始终是最新的。加上它提供的所有好处,测试Observable并不是一件简单的任务,不仅仅是简单地将XCTAssert用于普通值。但请放心-本文将指导您成为测试RxSwift的专家!
本文将教您如何为Observable线程创建单元测试。您将学习一些测试RxSwift代码的可用技术,以及一些技巧。开始吧。
注意:本文假定您已经熟悉RxSwift以及如何使用XCTest编写简单的测试。
入门
当您需要更改内容时,反应性应用程序的性能很好,因此我们将仅使用此类应用程序。
下载启动程序项目。您将找到本教程的入门项目:Raytronome是一个节拍器应用程序,可用于练习音乐。如您所知,由于节拍器会随着时间的推移而工作,因此您必须看到许多可以测试的逻辑。
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 .
在RxSwift和RxBlocking中都可以学到更多东西 -它们的内部工作,运算符等。最好的选择是官方的RxSwift文档以及RxBlocking运算符列表。
如果您对文章的内容有任何疑问或评论-欢迎发表评论或在原始文章的讨论中。谢谢阅读!警告,本文摘自raywenderlich.com的文章。