Testing Your RxSwift Code
In this tutorial, you’ll learn the key to testing RxSwift code. Specifically, you’ll learn how to create unit tests for your Observable streams. By Shai Mishali.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Testing Your RxSwift Code
35 mins
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!
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:
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 own TestScheduler
so that you can ensure that the beats are properly emitted on it.
- 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]