قالب وردپرس درنا توس
Home / IOS Development / Testing Your RxSwift Code | raywenderlich.com

Testing Your RxSwift Code | raywenderlich.com



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 will not usually have a singular value but are, instead , represented as a stream or 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 lets you, as a developer, respond to changes to ensure that your app is updated at all times. Als veel van een voordeel als dit biedt, testen streams of waarden zijn niet zo triviaal als gewoon asserting a single value. But worry not ̵

1; this tutorial will put you on your way to becoming an RxSwift-testing expert!

This tutorial will teach you how to create unit tests for your Observable streams. Du vil lære om noen av de tilgjengelige teknikker for testing av din RxSwift-kode, så vel som noen tips og 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 will 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 startup 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 will find lots of interesting pieces of logic and information to test here.

Open Raytronome.xcworkspace . Then open Main.storyboard .

Build and run the app. Tap the Play button to start the metronome.

 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 will write tests for.

The Challenges of Testing Streams

Here is a quick recap of the basics of RxSwift and Observable streams.

Working with streams is inherently different from working with basic values ​​and objects; Således er oppgaven med å teste dem annerledes, så godt.

Values ​​are single and independent; de har ikke noen repræsentation eller koncept av tid. Observable streams, on the other hand, emit elements (eg values) over time.

 Values ​​vs Observable Streams

This means that when testing streams of values, you

  • 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. 19659002] Open MetronomeViewModel.swift . Når du ser på det visningsmodul, kan du se outputs ansvarlige for flere stykker af logik: tælleren, denominatoren, signaturen og tempo strings, den tællerens aktuelle værdi, den maksimale værdi for tælleren, samt de ansvarlige streams. [19659002] The app uses instances of Driver to represent all outputs. A Driver is a kind of stream that makes your life easier when dealing with UI components.

 MetronomeViewModel's Inputs and Outputs

Let's think about what you would like to test in the UI. Make a quick list; You want to test that:

  • The numerator and denominator starts 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

RxBacking and RxTest . When you write your tests, you will use two additional frameworks bundled with RxSwift, called . Hver tilbyder forskellige kapaciteter og koncepter til testning dine streams.

The startup 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 (19459008) method for creating a new instance of our view model, MetronomeViewModel before each 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

 Blocking Observable Stream

It proves useful for situations in which you are dealing with a terminating sequence – meaning, one that emits a completed or error event – or aiming to test a final 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 item. 

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 following two tests to verify that the signatureText starts as 4/4 while tempoText starts as 120 BPM . De tests moeten bijna identiek zijn aan de twee hierboven.

If you get stuck, feel free to look at the solution by tapping the Once you're done, run your entire test suite again to make sure that you're good to go with four passing tests. ] 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.

  1. RxBlocking 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. RxBlocking will not provide the flexibility you need.
  2. In the more common case of dealing with non-terminating sequences, using
    RxBlocking works by blocking the current thread and actually locking the run-loop. If your Observable schedule events with relatively long intervals or delays, your BlockingObservable will wait for those in a synchronous matter.
  3. When you are interested in asserting time-based events and RxBlocking will not help if it only captures elements and not their times.
  4. When testing outputs depend on asynchronous input, RxBlocking will not Be useful as it blocks the current thread, for example, when testing a 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 issue of the isPlaying output, and this requires an asynchronous trigger (the tappedPlayPause input).

Using RxTest

As mentioned in the last section, RxBlocking provides great benefits, but it may be a bit lacking when it comes to grondig testen van uw stream's gebeurtenissen, tijden en relaties met andere asynchrone triggers.

RxTest RxTest RxTest comes to the rescue!

RxTest is an entirely different beast to RxBlocking with fastly more flexible in its capabilities and in the information that it provides about your streams. .

 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 Observable s and Observe s so that you can "record" these events and test them.

If you're interested

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:

 was scheduler: TestScheduler!
was disposeBag: DisposeBag!

Then, at the end of setup () add the following lines to create a new TestScheduler and DisposeBag before each test:

 scheduler = TestScheduler (initialClock: 0)
disposeBag = DisposeBag ()

The TestScheduler 's initializer takes in an initialClock argument that defines the "start 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" in the tappedPlayPause input.
  2. Create a mock Observer -like type of two 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 (city: disposeBag)

  // 3
  scheduler.createColdObservable ([.next(10, ()),
                                  .next(20, ()),
                                  .next(30, ())])
           .bind (to: viewModel.tappedPlayPause)
           .disposed (city: disposeBag)

  // 4
  scheduler.start ()

  // 5
  XCTAssertEqual (isPlaying.events, [
    .next(0, false),
    .next(10, true),
    .next(20, false),
    .next(30, true)
  ])
}

Do not 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 . events property that you can use to assert any events added to it.
  2. One of the main advantages of this special observer is that it exposes an
    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. 20 and 30 correspond to the times your inputs were filed, and 0 is the initial issue 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.

 TestableObserver, TestableObservable, Asserting timed events

Run your tests again by pressing Command- U . You have 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 (eg, 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 would like - they are completely arbitrary and TestScheduler uses them to schedule

10 does not actually mean 10 seconds, but any other scheduler.

One important thing to keep in mind is that this virtual time does not actually correspond to actual seconds, meaning, only represents a virtual time.

TestScheduler hvorfor ikke du gå tilbage til at tilføje mere test coverage for your view model?

Add the following three tests immediately after the previous one:

 func testModifyingNumeratorUpdatesNumeratorText () {
  easy numerator = scheduler.createObserver (String.self)

  viewModel.numeratorText
           .drive (numerator)
           .disposed (city: disposeBag)

  scheduler.createColdObservable ([.next(10, 3),
                                  .next(15, 1)])
           .bind (to: viewModel.steppedNumerator)
           .disposed (city: 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 (city: 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 (city: disposeBag)

  scheduler.start ()

  XCTAssertEqual (denominator.events, [
    .next(0, "4"),
    .next(10, "8"),
    .next(15, "32"),
    .next(20, "16"),
    .next(25, "4")
  ])
}

func testModifyingTempoUpdatesTempoText () {
  slow pace = scheduler.createObserver (String.self)

  viewModel.tempoText
           .drive (PACE)
           .disposed (city: disposeBag)

  scheduler.createColdObservable ([.next(10, 75),
                                  .next(15, 90),
                                  .next(20, 180),
                                  .next(25, 60)])
           .bind (to: viewModel.tempo)
           .disposed (city: 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 at this time, as it is quite similar to the previous test. You mock changing the numerator to 3 and then 1 . And you claim 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. Note that the numerator values ​​are actually 1 through 4, while the actual presentation is 4 8 16 and 32 . [19659002] Finally, you claim 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 - it seems like you've 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 (city: 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 (city: 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 (city: 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! Dette er virkelig ikke noe nytt eller skræmmende, men bare en lengre variant av de samme testene som du skrev så langt. You're adding elements to both the steppedNumerator and steppedDenominator inputs consecutively to create all sorts of different time signatures, and then you claim that the signatureText output emits properly formatted signatures.

Multiple inputs / single output RxTest " width="1698" height="558" class="aligncenter size-full wp-image-203442 bordered"/>

Feel free to run your test suite again.

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 [19459008
  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 are not valid meters for your metronome.

Note : Even though some of these meters are valid musically, you will consider them illegal for the sake of your metronome.

Add a test for this scenario:

 func testModifyingDenominatorUpdatesNumeratorValueIfExceedsMaximum () {
  // 1
  light teller = scheduler.createObserver (Double.self)

  viewModel.numeratorValue
           .drive (numerator)
           .disposed (city: 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 (city: disposeBag)

  scheduler.createColdObservable ([.next(10, 24)])
           .bind (to: viewModel.steppedNumerator)
           .disposed (city: 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 not 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.
  3. You claim 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.

Output stays on 24 even when the denominator drops down 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, get 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 will 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 keep 10 beautifully executed tests. Amazing work!

 Success! Awesome work.

Time-Sensitive Testing

You've got pretty far with testing your view model. Når du ser på din coverage rapport, vil du se at du har omkring 78% testdækning af dit visningsmodel. Time to take it all the way to the top!

Note : To see the code coverage, select Edit Scheme ... from the Scheme popup and, in the Test section, select the Options tab and then check Code Coverage . Choose Gather coverage for some targets and add the Raytronome target to the list. Efter den næste testkørsel vil defineringsdata være tilgængelige i Report navigator.

There are two final pieces to test to wrap up this tutorial.

You want to test that, given some meters / 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 will start 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 ()
    .takes (8)
    .bind (to: beat)
    .disposed (city: disposeBag)

  // 3
  scheduler.start ()

  XCTAssertEqual (beat.events, [])
}

This test is not 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 two schedule beats for the app, but when testing the beat, you'll want to inject your own TestScheduler so that you can ensure at the beats are properly emitted on it.

    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.

  2. Start the scheduler.

Run your tests by pressing Command-U . Notice that you are claiming against an empty array, knowing the test will fail - mainly to see what values ​​and times you are getting. 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, do not they? Simply a list of numbers from one to 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! Merk at du har akkurat de samme tidene for de emitterte hendelsene. 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 rate of 120 BPM and using a denominator of 4 (such as for 4/4 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 1but rounding up 0.5 / 1 will also be equal to 1which is simply not accurate enough for this sort of test.

Fortunately, you can change the resolutionproviding 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-Uand 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.1as that provides enough accuracy for the 4 denominator.

Run your test suite one final time by pressing Command-Uand 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 MetronomeViewModelwhich 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.


Source link