OlĂĄ Habr! Apresento a vocĂȘ a tradução do artigo "Testing Your RxSwift Code", de Shai Mishali, de raywenderlich.com .
Escrever aplicativos reativos com o RxSwift difere conceitualmente de escrever aplicativos da "maneira usual". Difere no sentido de que os objetos em seu aplicativo geralmente nĂŁo terĂŁo um significado claro; em vez disso, serĂŁo representados por um fluxo de valores no eixo do tempo, conhecido como RxSwift Observable
. Este artigo fornece a chave para testar o cĂłdigo RxSwift.
Observable
- Um mecanismo poderoso que permite que vocĂȘ, como desenvolvedor, responda Ă s alteraçÔes e verifique se o estado do seu aplicativo estĂĄ sempre atualizado. Juntamente com todos os benefĂcios que ele oferece, o teste Observable
nĂŁo Ă© uma tarefa trivial como simplesmente usar o XCTAssert para valores comuns. Mas nĂŁo se preocupe: este artigo o ajudarĂĄ a se tornar um especialista em testar o RxSwift!
Este artigo ensinarĂĄ como criar testes de unidade para Observable
threads. VocĂȘ aprenderĂĄ algumas das tĂ©cnicas disponĂveis para testar o cĂłdigo RxSwift, bem como algumas dicas. Vamos começar.
Nota: Este artigo pressupĂ”e que vocĂȘ jĂĄ esteja familiarizado com o RxSwift e como escrever testes simples usando o XCTest.
Começando
Os aplicativos reativos tĂȘm bom desempenho quando vocĂȘ precisa trabalhar com alteraçÔes de conteĂșdo, portanto, trabalharemos exatamente com esse aplicativo.
Faça o download do projeto inicial . VocĂȘ encontrarĂĄ o projeto inicial deste tutorial: Raytronome Ă© um aplicativo de metrĂŽnomo que vocĂȘ pode usar para praticar mĂșsica. Como vocĂȘ pode entender, devido ao fato de os metrĂŽnomos funcionarem ao longo do tempo, vocĂȘ precisa ver muita lĂłgica que pode ser testada.
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
.
Muito mais pode ser aprendido no RxSwift e no RxBlocking - seu trabalho interno, operadores e assim por diante. O melhor lugar para continuar é a documentação oficial do RxSwift , bem como uma lista de operadores do RxBlocking .
Se vocĂȘ tiver alguma dĂșvida ou comentĂĄrio sobre o conteĂșdo do artigo - seja bem-vindo aos comentĂĄrios ou na discussĂŁo do artigo original . Obrigado pela leitura! Atenção , este artigo Ă© uma tradução do artigo de raywenderlich.com .