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.

5 (17) · 1 Review

Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

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:

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.

  1. 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 the tappedPlayPause 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 own TestScheduler so that you can ensure that the beats are properly emitted on it.

  2. Create a TestableObserver for the Beat type and record the first 8 beats of the beat output from the view model. 8 beats represent two rounds, which should be enough to make sure everything is emitted properly.
  3. 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:

VirtualTimeUnit conversion with different 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.

99.25% View Model coverage

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]