Testing your RxSwift code, part 2

In part 1 I looked into writing very basic tests with RxSwift and life was good. But then I wanted to do more and moved on to writing asynchronous tests…

I contributed the code for RxRealm - the Rx extension for RealmSwift owned by the RxSwiftCommunity. For RxRealm I needed some asynchronous tests because Realm’s collections emit notifications (which RxRealm wraps) when the underlaying data changes.

So let’s have a look at some of the unit tests I wrote…

Asynchronous tests

First of all I needed to look into writing asynchronous tests - luckily this is quite easy. The basic structure of such a test is:

func testSomething() {
  let expectation1 = expectationWithDescription("First event")
  let expectation2 = expectationWithDescription("Second event")

  ... whatever code you need to perform the test ... 
  
  someAsyncAPI(didComplete: {
    expectation1.fulfill()
  })

  someOtherAsyncAPI(didComplete: {
    expectation2.fulfill()
  })
  
  waitForExpectationsWithTimeout(1.0) {error in
     XCTAssertTrue(error == nil)
     ... more asserts ... 
  }
}

Let’s look at what this code does: * you need to define all the expectations you’d like this test to wait for before it completes; the string argument is decorative, * whenever the events you are waiting for complete you call fulfill() on your expectations, * finally you need to define a block to be executed when ALL the expectations are fulfilled; waitForExpectationsWithTimeout ’s first parameter is a timeout, which will force the block to execute if the expectations do not fulfill in time.

Combine TestScheduler and async tests

Let’s look at a complete test from the RxRealm test suite.

First I start by defining a single expectation - my expectation will be that a precise number of events were emitted by the observable.

    
func testResultsTypeChangeset() {
    let expectation1 = expectationWithDescription("Results")

    let realm = realmInMemory(#function)
    clearRealm(realm)

}

Additionally I use two of my helper methods to grab a reference to an in-memory realm and clear all objects that might’ve been stored inside by other tests in the suite.

Now just like in Part 1 I define a dispose bag and a test scheduler:

let bag = DisposeBag()
        
let scheduler = TestScheduler(initialClock: 0)
let observer = scheduler.createObserver(String)

Note that I’ll be observing a sequence of Strings - you’ll see why in a minute.

Fulfilling expectations

Next I need to create a Results object since I’m going to be testing Results.asObservable():

let messages$ = realm.objects(Message).asObservableChangeset().shareReplay(1)

I’m observing all Message objects stored in my realm.

Now comes the interesting part - first I’ll define when my expectation is fulfilled:

messages$
  .scan(0, accumulator: {acc, _ in return acc+1})
  .filter { $0 == 3 }
  .map {_ in ()}
  .subscribeNext(expectation1.fulfill)
  .addDisposableTo(bag)

I use scan to count the emitted values from my observable, filter helps me to pinpoint in which value I’m interested (the 3rd event), I map the value to Void and subscribe to expectation1.fulfill().

Long story short - when messages$ emits its 3rd event my expectation will fulfill. Neat!

Since I’m not interested in just how many events are emitted but also which events were emitted, I need yet another subscription:

messages$
  .map {results, changes in
    if let changes = changes {
      return "i:\(changes.inserted) d:\(changes.deleted) u:\(changes.updated)"
    } else {
      return "initial"
    }
  }
  .subscribe(observer)
  .addDisposableTo(bag)

If it’s an update event I return a string with the collection indexes that were deleted or inserted, it looks like so:

i:[0] u:[] d:[1,2,4]

This logs all the information about the particular change-set that I’m interested in (and need to verify if the observable emitted the information I expected).

If it’s the initial event for that collection I just return “initial”.

Triggering some Observable values

Now I can start the test scheduler:

scheduler.start()

And, ultimately, perform some operations on my Realm to produce change notifications:

delay(0.1) {
  try! realm.write {
    realm.add(Message("first"))
  }
}
delay(0.2) {
  try! realm.write {
    realm.delete(realm.objects(Message).first!)
  }
}

The code above adds a new object and after a bit of delay deletes it again. This should produce the 3 events I’m expecting: the initial data, the insert notification and the deletion notification.

Verifying the recorded events with asserts

It’s time to (finally) add my asserts:

waitForExpectationsWithTimeout(0.5) {error in
    //do tests here
    
    XCTAssertTrue(error == nil)
    XCTAssertEqual(observer.events.count, 3)
    XCTAssertEqual(observer.events[0].value.element!, "initial")
    XCTAssertEqual(observer.events[1].value.element!, "i:[0] d:[] u:[]")
    XCTAssertEqual(observer.events[2].value.element!, "i:[] d:[0] u:[]")
}

In the block I perform a number of asserts:

  • no error happened while fulfilling my expectations
  • the test observer logged exactly 3 events
  • the first emitted value is “initial”
  • the second value is about inserting a value at index 0
  • the third value is about deleting a value at index 0

Conclusion

It’s amazing how easy writing tests is even when testing two completely de-coupled libraries working together like RealmSwift and RxSwift. As you saw it’s a matter of 10-20 lines of code depending on your setup and I’m sure the code can be simplified even more.

If you want to read through the complete test suite for RxRealm head to the GitHub repo test folder: RxRealm_Tests.

I hope this post has been useful! Do you know a better way to do any of this? Seen a bug? Ping me on Twitter at icanzilb

Hope that post was helpful, and if you want to get in touch you can find me here

Share this post:


If you'd like to learn how to create professional production apps with RxSwift, the best resource out there is the RxSwift book written by Florent Pillet, Junior Bontognali, Marin Todorov, & Scott Gardner.

It features 20+ chapters covering the basics, the Rx operators, and advanced topics like testing, error handling, and app architecture.

Available from Ray Wenderlich: » Learn more.