Testing Your RxSwift Code [FREE]
source link: https://www.tuicool.com/articles/hit/v67bMrv
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Writing reactive apps with RxSwift is a conceptually different task than writing apps “the regular way.” It’s different in the sense that things in your app won’t usually have a singular value but are, instead, represented as a stream of values over the axis of time, known within the RxSwift library as an Observable
. This tutorial teaches you the key to testing RxSwift code.
Streams are a powerful mechanism that let you, as a developer, react to changes to ensure that your app is updated at all times. As much of an advantage as this provides, testing streams of values is not as trivial as simply asserting a single value. But worry not — this tutorial will set you on your way to becoming an RxSwift-testing expert!
This tutorial will teach you how to create unit tests for your Observable
streams. You’ll learn some of the available techniques for testing your RxSwift code, as well as some tips and tricks. Let’s get started.
XCTest
.
If you’re interested in learning more about building reactive apps with RxSwift, you might want to look into our book: RxSwift: Reactive Programming with Swift. .
Getting Started
Since reactive apps really shine when dealing with changing content, of course you’ll deal with testing an app of that nature!
Use the Download Materials button at the top or bottom of this tutorial. You’ll find the starter project for this tutorial: Raytronome , a fun metronome app you could use to practice your musical accuracy. As you can imagine, since metronomes deal with time, you’ll find a lot of interesting pieces of logic and information to test here.
Open Raytronome.xcworkspace . Then open Main.storyboard . You’ll see it’s a very simple app with only a single screen.
Build and run the app. Tap the Play button to start the metronome. You can also change the time signature and tempo.
The app consists of a single view controller — MetronomeViewController.swift — and MetronomeViewModel.swift contains all the business logic, which is what you’ll write tests for.
The Challenges of Testing Streams
Here’s a quick recap of the basics of RxSwift and Observable
streams.
Working with streams is inherently different from working with basic values and objects; thus, the task of testing them is different, as well.
Values are single and independent; they don’t have any representation or concept of time. Observable
streams, on the other hand, emit elements (e.g. values) over time.
This means that, when testing streams of values, you’ll often need to test that either:
- Some stream emits specific elements, regardless of time.
- Some stream emits specific elements, at specific times . In this case, you’ll need a way to “record” these emitted elements along with when the stream emitted them.
Determining What to Test
It’s usually a good idea to take a few moments to think about what you actually want to test.
As mentioned earlier, you’ll test MetronomeViewModel
, the view model containing the actual business logic related to your metronome.
Open MetronomeViewModel.swift . Looking into the view model, you can see outputs responsible for several pieces of logic: the numerator, denominator, signature and tempo strings, the numerator’s actual value, the maximum value for the numerator, as well as streams responsible for the beat.
The app uses instances of Driver
to represent all outputs. A Driver
is a kind of stream which makes your life easier when dealing with UI components.
Let’s think about what you would want to test in the UI. Make a quick list; you want to test that:
- The numerator and denominator start at 4 and 4 .
- The signature starts at 4/4 .
- The tempo starts at 120 .
- Tapping the Play/Pause button changes the
isPlaying
state of the metronome. - Modifying the numerator, denominator or tempo emits proper textual representations.
- The beat is “beating” according to time signature.
- The beat alternates between
.even
and.odd
— the app uses this to set the metronome image at the top of the view.
When writing your tests, you’ll use two additional frameworks bundled with RxSwift, called RxBlocking and RxTest . Each offers different capabilities and concepts for testing your streams. These frameworks are already part of your starter project.
Using RxBlocking
The starter project includes a bare-bones test target with a RaytronomeTests.swift file.
Open it and look around; it imports RxSwift , RxCocoa , RxTest and RxBlocking , and it includes a viewModel
property and a basic setUp()
method to create a new instance of our view model, MetronomeViewModel
before every test case.
Your first test cases will be about making sure the numerator and denominator both start with a value of 4
. Meaning, you’ll only care about the first emitted value of each of these streams. Sounds like a perfect job for RxBlocking !
RxBlocking is one of the two testing frameworks available with RxSwift, and it follows a simple concept: It lets you convert your Observable
stream to a BlockingObservable
, a special observable that blocks the current thread, waiting for specific terms dictated by its operators.
It proves useful for situations in which you’re dealing with a terminating sequence — meaning, one that emits a completed
or error
event — or aiming to test a finite number of events.
RxBlocking provides several operators, with the most useful ones being:
toArray() first() last()
Looking through these operators, first()
is the one that is most suitable for this specific case.
Add the following two test cases to the RaytronomeTests
class:
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") }
You use toBlocking()
to convert your regular stream to a BlockingObservable
and then use first()
to wait for and return the first emitted element. You can then assert against it, like you would on any other regular test.
Notice that the test methods include throws
in their signatures, since RxBlocking’s operators may throw. Annotating the test method itself with throws
is useful for avoiding try!
and for gracefully failing the test if it throws an exception internally.
Press Command-U to run your tests.
As a quick challenge, try and write the next two tests to verify that the signatureText
starts as 4/4
, while tempoText
starts as 120 BPM
. The tests should be almost identical to the two above.
Once you’re done, run your entire test suite again to make sure that you’re good to go with four passing tests.
If you get stuck, feel free to peek at the solution by tapping the Reveal button:
[spoiler title=”Tests for Signature and Tempo”]
func testSignatureStartsAt4By4() throws { XCTAssertEqual(try viewModel.signatureText.toBlocking().first(), "4/4") } func testTempoStartsAt120() throws { XCTAssertEqual(try viewModel.tempoText.toBlocking().first(), "120 BPM") }
[/spoiler]
Advantages and Disadvantages of RxBlocking
As you might have noticed, RxBlocking is great and is easy to get started with as it sort of “wraps” the reactive concepts under very well-known constructs. Unfortunately, it comes with a few limitations that you should be aware of:
- It’s aimed at testing finite sequences, meaning that, if you want to test the first element or a list of elements of a completed sequence, RxBlocking will prove to be very useful. However, in the more common case of dealing with non-terminating sequences, using RxBlocking won’t provide the flexibility you need.
- RxBlocking works by blocking the current thread and actually locking the run-loop. If your
Observable
schedules events with relatively long intervals or delays, yourBlockingObservable
will wait for those in a synchronous matter. - When you’re interested in asserting time-based events and confirming they contain the correct time stamp, RxBlocking is no help as it only captures elements and not their times.
- When testing outputs that depend on asynchronous input, RxBlocking won’t be useful as it blocks the current thread, for example, when testing an output that needs some other observable trigger to emit.
The next tests you need to implement run into most of these limitations. For example: Tapping the Play/Pause button should cause a new emission of the isPlaying
output, and this requires an asynchronous trigger (the tappedPlayPause
input). It would also be beneficial to test the times of the emissions.
Using RxTest
As mentioned in the last section, RxBlocking provides great benefits, but it might be a bit lacking when it comes to thoroughly testing your stream’s events, times and relations with other asynchronous triggers.
To resolve all of these issues, and more, RxTest comes to the rescue!
RxTest is an entirely different beast to RxBlocking , with the main difference being that it is vastly more flexible in its abilities and in the information that it provides about your streams. It’s able to do this because it provides its very own special scheduler called TestScheduler
.
Before diving into code, it’s worthwhile to go over what a scheduler actually is.
Understanding Schedulers
Schedulers are a bit of a lower-level concept of RxSwift, but it’s important to understand what they are and how they work, to better understand their role in your tests.
In RxSwift, you use schedulers to abstract and describe how to perform work, as well as to schedule the emitted events resulting from that work.
Why is this interesting, you might ask?
RxTest provides its own custom scheduler called TestScheduler
solely for testing. It simplifies testing time-based events by letting you create mock Observable
s and Observer
s so that you can “record” these events and test them.
If you’re interested in diving deeper into schedulers, the official documentation offers some great insights and guidelines.
Writing Your Time-Based Tests
Before writing your tests, you’ll need to create an instance of TestScheduler
. You’ll also add a DisposeBag
to your class to manage the Disposables
that your tests create. Below your viewModel
property, add the following properties:
var scheduler: TestScheduler! var disposeBag: DisposeBag!
Then, at the end of setUp()
, add the following lines to create a new TestScheduler
and DisposeBag
before every test:
scheduler = TestScheduler(initialClock: 0) disposeBag = DisposeBag()
The TestScheduler
‘s initializer takes in an initialClock
argument that defines the “starting time” for your stream. A new DisposeBag
will take care of getting rid of any subscriptions left by your previous test.
Onward to some actual test writing!
Your first test will trigger the Play/Pause button several times and assert the isPlaying
output emits changes accordingly.
To do that, you need to:
- Create a mock
Observable
stream emitting fake “taps” into thetappedPlayPause
input. - Create a mock
Observer
-like type to record events emitted by theisPlaying
output. - Assert the recorded events are the ones that you expect.
This might seem like a lot, but you’ll be surprised at how it comes together!
Some things are better explained with an example. Start by adding your first RxTest -based test:
func testTappedPlayPauseChangesIsPlaying() { // 1 let isPlaying = scheduler.createObserver(Bool.self) // 2 viewModel.isPlaying .drive(isPlaying) .disposed(by: disposeBag) // 3 scheduler.createColdObservable([.next(10, ()), .next(20, ()), .next(30, ())]) .bind(to: viewModel.tappedPlayPause) .disposed(by: disposeBag) // 4 scheduler.start() // 5 XCTAssertEqual(isPlaying.events, [ .next(0, false), .next(10, true), .next(20, false), .next(30, true) ]) }
Don’t worry if this looks a bit intimidating. Breaking it down:
- Use your
TestScheduler
to create aTestableObserver
of the type of elements that you want to mock — in this case, aBool
. One of the main advantages of this special observer is that it exposes anevents
property that you can use to assert any events added to it. -
drive()
yourisPlaying
output into the newTestableObserver
. This is where you “record” your events. - Create a mock
Observable
that mimics the emission of three “taps” into thetappedPlayPause
input. Again, this is a special type ofObservable
called aTestableObservable
, which uses yourTestScheduler
to emit events on the provided virtual times. - Call
start()
on your test scheduler. This method triggers the pending subscriptions created in the previous points. - Use a special overload of
XCTAssertEqual
bundled with RxTest , which lets you assert the events inisPlaying
are equal, in both elements and times, to the ones you expect.10
,20
and30
correspond to the times your inputs fired, and0
is the initial emission ofisPlaying
.
Confused? Think about it this way: You “mock” a stream of events and feed it into your view model’s input at specific times. Then, you assert your output to make sure that it emits the expected events at the right times.
Run your tests again by pressing Command-U . You should see five passing tests.
Understanding Time Values
You’ve probably noticed the 0
, 10
, 20
and 30
values used for time and wondered what these values actually mean. How do they relate to actual time?
RxTest uses an internal mechanism for converting regular time (e.g., a Date
) into what it calls a VirtualTimeUnit
(represented by an Int
).
When scheduling events with RxTest , the times that you use can be anything that you’d like — they are entirely arbitrary and TestScheduler
uses them to schedule the events, like any other scheduler.
One important thing to keep in mind is that this virtual time doesn’t actually correspond with actual seconds, meaning, 10
doesn’t actually mean 10 seconds, but only represents a virtual time. You’ll learn a bit more about the internals of this mechanism later in this tutorial.
Now that you have a deeper understanding of times in TestScheduler
, why don’t you go back to adding more test coverage for your view model?
Add the following three tests immediately after the previous one:
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) // Denominator is 2 to the power of `steppedDenominator + 1`. // f(1, 2, 3, 4) = 4, 8, 16, 32 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") ]) }
These tests do the following:
testModifyingNumeratorUpdatesNumeratorText testModifyingDenominatorUpdatesNumeratorText testModifyingTempoUpdatesTempoText
Hopefully, you feel right at home with this code by now as it is quite similar to the previous test. You mock changing the numerator to 3
, and then 1
. And you assert the numeratorText
emits "4"
(initial value of 4/4 signature), "3"
, and eventually "1"
.
Similarly, you test that changing the denominator’s value updates denominatorText
, accordingly. Notice that the numerator values are actually 1 through 4, while the actual presentation is 4
, 8
, 16
, and 32
.
Finally, you assert that updating the tempo properly emits a string representation with the BPM
suffix.
Run your tests by pressing Command-U , leaving you with a total of eight passing tests. Nice!
OK — seems you like you got the hang of it!
Time to step it up a notch. Add the following test:
func testModifyingSignatureUpdatesSignatureText() { // 1 let signature = scheduler.createObserver(String.self) viewModel.signatureText .drive(signature) .disposed(by: disposeBag) // 2 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) // Denominator is 2 to the power of `steppedDenominator + 1`. // f(1, 2, 3, 4) = 4, 8, 16, 32 scheduler.createColdObservable([.next(15, 2), // switch to 8ths .next(30, 3), // switch to 16ths .next(40, 4) // switch to 32nds ]) .bind(to: viewModel.steppedDenominator) .disposed(by: disposeBag) // 3 scheduler.start() // 4 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") ]) }
Take a deep breath! This really isn’t anything new or terrifying but merely a longer variation of the same tests that you wrote so far. You’re adding elements onto both the steppedNumerator
and steppedDenominator
inputs consecutively to create all sorts of different time signatures, and then you are asserting that the signatureText
output emits properly formatted signatures.
This becomes clearer if you look at the test in a more visual way:
Feel free to run your test suite again. You now have 9 passing tests!
Next, you’ll take a crack at a more complex use case.
Think of the following scenario:
- The app starts with a
4/4
signature. - You switch to a
24/32
signature. - You then press the – button on the denominator; this should cause the signature to drop to
16/16
, then8/8
and, eventually,4/4
, because24/16
,24/8
and24/4
aren’t valid meters for your metronome.
Note : Even though some of these meters are valid musically, you’ll consider them illegal for the sake of your metronome.
Add a test for this scenario:
func testModifyingDenominatorUpdatesNumeratorValueIfExceedsMaximum() { // 1 let numerator = scheduler.createObserver(Double.self) viewModel.numeratorValue .drive(numerator) .disposed(by: disposeBag) // 2 // Denominator is 2 to the power of `steppedDenominator + 1`. // f(1, 2, 3, 4) = 4, 8, 16, 32 scheduler.createColdObservable([ .next(5, 4), // switch to 32nds .next(15, 3), // switch to 16ths .next(20, 2), // switch to 8ths .next(25, 1) // switch to 4ths ]) .bind(to: viewModel.steppedDenominator) .disposed(by: disposeBag) scheduler.createColdObservable([.next(10, 24)]) .bind(to: viewModel.steppedNumerator) .disposed(by: disposeBag) // 3 scheduler.start() // 4 XCTAssertEqual(numerator.events, [ .next(0, 4), // Expected to be 4/4 .next(10, 24), // Expected to be 24/32 .next(15, 16), // Expected to be 16/16 .next(20, 8), // Expected to be 8/8 .next(25, 4) // Expected to be 4/4 ]) }
A bit complex, but nothing you can’t handle! Breaking it down, piece by piece:
- As usual, you start off by creating a
TestableObserver
and driving thenumeratorValue
output to it. - Here, things get a tad confusing, but looking at the visual representation below will make it clearer. You start by switching to a
32
denominator, and then switch to a24
numerator (on the second stream), putting you at a24/32
meter. You then drop the denominator step-by-step to cause the model to emit changes on thenumeratorValue
output. - Start the
scheduler
. - You assert that the proper
numeratorValue
is emitted for each of the steps.
Quite the complex test that you’ve made! Run your tests by pressing 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]") -
Oh, no! The test failed.
Looking at the expected result, it seems like the numeratorValue
output stays on 24
, even when the denominator drops down, leaving you with illegal signatures such as 24/16
or 24/4
. Build and run the app and try it yourself:
- Increase your denominator, leaving you at a 4/8 signature.
- Do the same for your numerator, getting to a 7/8 signature.
- Drop your denominator by one. You’re supposed to be at 4/4, but you’re actually at 7/4 — an illegal signature for your metronome!
Seems like you’ve found a bug. :]
Of course, you’ll make the responsible choice of fixing it.
Open MetronomeViewModel.swift and find the following piece of code responsible for setting up numeratorValue
:
numeratorValue = steppedNumerator .distinctUntilChanged() .asDriver(onErrorJustReturn: 0)
Replace it with:
numeratorValue = Observable .combineLatest(steppedNumerator, maxNumerator.asObservable()) .map(min) .distinctUntilChanged() .asDriver(onErrorJustReturn: 0)
Instead of simply taking the steppedNumerator
value and emitting it back, you combine the latest value from the steppedNumerator
with the maxNumerator
and map to the smaller of the two values.
Run your test suite again by pressing Command-U , and you should behold 10 beautifully executed tests. Amazing work!
Time-Sensitive Testing
You’ve gotten pretty far with testing your view model. Looking at your coverage report, you’ll see you have about 78% test coverage of your view model. Time to take it all the way to the top!
Note : To see the code coverage, select Edit Scheme… from the Scheme pop-up and, in the Test section, choose the Options tab and then check Code Coverage . Choose Gather coverage for some targets and add the Raytronome target to the list. After the next test run, coverage data will be available in the Report navigator.
There are two final pieces to test to wrap up this tutorial. The first of them is testing the actual beat emitted.
You want to test that, given some meter/signature, beats are emitted in evenly spaced intervals and also that the beat itself is correct (the first beat of each round is different from the rest).
You’ll start by testing the fastest denominator — 32
. Go back to RaytronomeTests.swift and add the following test:
func testBeatBy32() { // 1 viewModel = MetronomeViewModel(initialMeter: Meter(signature: "4/32"), autoplay: true, beatScheduler: scheduler) // 2 let beat = scheduler.createObserver(Beat.self) viewModel.beat.asObservable() .take(8) .bind(to: beat) .disposed(by: disposeBag) // 3 scheduler.start() XCTAssertEqual(beat.events, []) }
This test isn’t intended to pass, yet. But still breaking it down into smaller pieces:
- For this specific test, you initialize your view model with a few options. You start with a
4/32
meter and tell the view model to start emitting beats automatically, which saves you the trouble of triggering thetappedPlayPause
input.The third argument is also an important one. By default, the view model uses a
SerialDispatchQueueScheduler
to schedule beats for the app, but, when actually testing the beat, you’ll want to inject your ownTestScheduler
so that you can ensure that the beats are properly emitted on it. - Create a
TestableObserver
for theBeat
type and record the first8
beats of thebeat
output from the view model.8
beats represent two rounds, which should be enough to make sure everything is emitted properly. - Start the scheduler. Notice that you’re asserting against an empty array, knowing the test will fail — mainly to see what values and times you’re getting.
Run your tests by pressing Command-U . You’ll see the following output for the assertion:
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 ("[]") —
It seems that your events are emitting the correct values, but the times seem a bit strange, don’t they? Simply a list of numbers from one through eight.
To make sure this makes sense, try changing the meter from 4/32
to 4/4
. This should produce different times, as the beat itself is different.
Replace Meter(signature: "4/32")
with Meter(signature: "4/4")
and run your tests again by pressing Command-U . You should see the exact same assertion failure, with the exact same times.
Wow, this is odd! Notice that you got the exact same times for the emitted events. How is it that two different signatures emit on the, so-called, “same time”? Well, this is related to the VirtualTimeUnit
mentioned earlier in this tutorial.
Stepping Up the Accuracy
By using the default tempo of 120 BPM
, and using a denominator of 4
(such as for 4/4
), you should get a beat every 0.5
seconds. By using a 32
denominator (such as for 4/32
), you should get a beat every 0.0625
seconds.
To understand why this is an issue, you’ll need to better understand how TestScheduler
internally converts “real time” into its own VirtualTimeUnit
.
You calculate a virtual time by dividing the actual seconds by something called a resolution
and rounding that result up. resolution
is part of a TestScheduler
and defaults to 1
.
0.0625 / 1
rounded up would be 1
, but rounding up 0.5 / 1
will also be equal to 1
, which is simply not accurate enough for this sort of test.
Fortunately, you can change the resolution
, providing better accuracy for this sort of time-sensitive test.
Above the instantiation of your view model, on the first line of your test, add the following line:
scheduler = TestScheduler(initialClock: 0, resolution: 0.01)
This will decrease the resolution
and provide higher accuracy while rounding up the virtual time.
Notice how the virtual times are different, when dropping down the resolution:
Switch your meter back to 4/32
in the view model initializer and run your tests again by pressing Command-U .
You’ll finally get back more refined time stamps that you can assert against:
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 ("[]") —
The beats are evenly spaced by a virtual time of 6
. You can now replace your existing XCTAssertEqual
with the following:
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) ])
Run your tests one more time by pressing Command-U , and you should see this test finally passing. Excellent!
Using the same method for testing a 4/4
beat is very similar.
Add the following test:
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) ]) }
The only difference, here, is that you bumped the resolution up to 0.1
, as that provides enough accuracy for the 4
denominator.
Run your test suite one final time by pressing Command-U , and you should see all 12 tests pass at this point!
If you look into your view model’s coverage, you’ll notice you have 99.25%
coverage for MetronomeViewModel
, which is excellent. Only one output is not tested: the beatType
.
Testing the beat type would be a good challenge at this point, since it should be very similar to the previous two tests, except that the beat type should alternate between .even
and .odd
. Try writing that test by yourself. If you become stuck, press the Reveal button below to reveal the answer:
[spoiler title=”Beat Type Test”]
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) ]) }
[/spoiler]
Where to Go From Here?
You can download the completed version of the project using the Download Materials button at the top or bottom of this tutorial.
You now know everything you need to start testing your RxSwift-based apps. You learned how RxBlocking is useful in testing terminating sequences or sequences in which you’re not interested in when elements were emitted, while RxTest provides extra flexibility and power suited mostly to testing operators and time-based streams.
And you even touched a bit on some lower-level concepts such as the basics of schedulers and how RxTest’s TestScheduler
calculates virtual time.
There is still more to explore in regards to both RxBlocking and RxTest — their internal workings, operators and more. The best place to continue with your studies will be the official RxSwift Unit Tests documentation , as well as RxBlocking’s operator list .
In the meantime, if you have any questions or comments about this tutorial or writing tests for your RxSwift code in general, please join the forum discussion below!
Thanks to Guy Magen for his awesome work designing this app and really making it shine. You can find some of his work at https://www.guymagen.com .
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK