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.

Version

  • Swift 4.2, iOS 12, Xcode 10

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.

Note: This tutorial assumes that you are already knowledgeable with how to use RxSwift, as well as how to write basic tests using 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.

Basic Raytronome app

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.

Values vs Observable Streams

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.

MetronomeViewModel's Inputs and Outputs

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.

Blocking Observable Stream

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(): Wait for the sequence to terminate and return all results as an array.
  • first(): Wait for the first element and return it.
  • last(): Wait for the sequence to terminate and return the last item emitted.

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.

Testing RxSwift code

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:

  1. 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.
  2. RxBlocking works by blocking the current thread and actually locking the run-loop. If your Observable schedules events with relatively long intervals or delays, your BlockingObservable will wait for those in a synchronous matter.
  3. 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.
  4. 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.

RxTest - Measuring timed events

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.

RxSwift uses 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 Observables and Observers 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:

  1. Create a mock Observable stream emitting fake “taps” into the tappedPlayPause input.
  2. Create a mock Observer-like type to record events emitted by the isPlaying output.
  3. 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:

  1. Use your TestScheduler to create a TestableObserver of the type of elements that you want to mock — in this case, a Bool. One of the main advantages of this special observer is that it exposes an events property that you can use to assert any events added to it.
  2. drive() your isPlaying output into the new TestableObserver. This is where you “record” your events.
  3. Create a mock Observable that mimics the emission of three “taps” into the tappedPlayPause input. Again, this is a special type of Observable called a TestableObservable, which uses your TestScheduler to emit events on the provided virtual times.
  4. Call start() on your test scheduler. This method triggers the pending subscriptions created in the previous points.
  5. Use a special overload of XCTAssertEqual bundled with RxTest, which lets you assert the events in isPlaying are equal, in both elements and times, to the ones you expect. 10, 20 and 30 correspond to the times your inputs fired, and 0 is the initial emission of isPlaying.

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.

TestableObserver, TestableObservable, Asserting timed events

Run your tests again by pressing Command-U. You should see five passing tests.

5 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: Tests that when you modify the numerator, the text updates correctly.
  • testModifyingDenominatorUpdatesNumeratorText: Tests that when you modify the denominator, the text updates correctly.
  • testModifyingTempoUpdatesTempoText: Tests that when you modify the tempo, the text updates correctly.

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!

8 Passing Tests

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:

Multiple inputs / single output RxTest

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:

  1. The app starts with a 4/4 signature.
  2. You switch to a 24/32 signature.
  3. You then press the button on the denominator; this should cause the signature to drop to 16/16, then 8/8 and, eventually, 4/4, because 24/16, 24/8 and 24/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:

  1. As usual, you start off by creating a TestableObserver and driving the numeratorValue output to it.
  2. 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 a 24 numerator (on the second stream), putting you at a 24/32 meter. You then drop the denominator step-by-step to cause the model to emit changes on the numeratorValue output.
  3. Start the scheduler.
  4. You assert that the proper numeratorValue is emitted for each of the steps.

Two inputs, One output skipping RxTest

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!

Invalid meters not handled properly

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!

Success! Awesome 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:

  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]

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.

Contributors

Comments