Unit Testing RxJava: From Beginner to Competent User

RxJava and Reactive Programming is a very hot topic in the Java and Android worlds and we follow it very closely here at Macoscope. Unfortunately, there aren’t all that many materials available about testing reactive code. I would like to share with you our findings on the subject and guide you through the most reasonable ways of unit testing RxJava-related code. This article is aimed primarily at developers who already have a basic working knowledge of developing applications with the help of RxJava and unit testing in general. Code examples are written in Groovy and use Spock framework as a test runner.

rxjava

Testing RxJava Results on the Same Thread

There are a couple of problems that we run into and have to manage while testing RxJava. First of all, the result of the task is passed to the callback and we have to check it inside of our test.

The most straightforward way to do that is to assign the result to a local variable and then assert its value. So, I start by creating a list that will store our result and then for each event callback I add the result to the list. At the end of the test I have a list with all events which can be asserted that were emitted by the tested observable.

def 'store result in variable'() {
   setup:
       List<String> result = new ArrayList<>()
   when:
       Observable.just("Macoscope", "Android Apps")
              .subscribe(new Action1<String>() {
           @Override
           void call(String s) {
               result.add(s)
           }
       });
   then:
       result.containsAll("Macoscope", "Android Apps")
}

It might not be a very elegant way of testing, but it works for this simple case.

Testing RxJava Results on a Different Thread

In most cases we have to deal with code that is executed on a separate thread. The simplest way to simulate this is to use the delay operator which uses the computation scheduler by default.

The Delay operator does not block the test thread, so we have to wait to get the test result somehow. The dirtiest way of doing that would be to adding the sleep(1000) call before the assertion. There is, however, a better way of doing that in Spock: we can use PollingConditions. In this example, it checks the given condition every 0.1 second and timeouts after 3 seconds. This allows us to forgo any assumptions as to how long it will take to execute the tested code. The test will finish immediately after the condition is met. Worst case scenario, it will wait until timeout and fail.

def 'store result with polling condition'() {
   setup:
       PollingConditions conditions = new PollingConditions(timeout: 3, initialDelay: 0, factor: 0.1)
       List<String> result = new ArrayList<>()
   when:
       Observable.just("Macoscope", "Android Apps")
               .delay(1000, MILLISECONDS)
               .subscribe(new Action1<String>() {
           @Override
           void call(String s) {
               result.add(s)
           }
       });
   then:
       conditions.eventually {
           result.containsAll("Macoscope", "Android Apps")
       }
}

Still, the test doesn’t look all that good as it is bloated with boilerplate code.

Converting Reactive Code to a Synchronous Call

Fortunately for us, there are better ways to test RxJava. While searching through Rx operators, you may have encountered the toBlocking operator. It gives us access to blocking operators that convert asynchronous code into a synchronous one.

I want to retrieve all of the events that were passed to the onNext callback. First, I convert the result to List in order to receive all the events, and then I use the toBlocking operator and call the blocking operator single. This, in turn, returns a result after stream completion. This way, after waiting about 1 second, I receive a list of results that can be asserted.

def 'convert to synchronous code'() {
   when:
       List<String> result = Observable.just("Macoscope", "Android Apps")
               .delay(1000, MILLISECONDS)
               .toList().toBlocking().single();
   then:
       result.containsAll("Macoscope", "Android Apps")
}

The code is much more concise now. There is, however, one problem with this particular approach: it only works for a finite stream of events. For streams that do not end, this test would hang for ever.

Testing RxJava Observables with TestSubscriber

There is also an official tool called TestSubscriber, which removes most of the boilerplate code and allows for testing of both finite and infinite streams.

TestSubscriber is a powerful implementation of regular Subscriber, which provides a number of helpful methods we can use to test RxJava observables. In our example, I am using awaitTerminalEvent in order to wait for stream completion and then I assert whether the given values were emitted by the observable.

def 'test subscriber'() {
   setup:
       TestSubscriber<String> testSubscriber = new TestSubscriber<>()
   when:
       Observable.just("Macoscope", "Android Apps")
               .delay(1000, MILLISECONDS)
               .subscribe(testSubscriber);
   then:
       testSubscriber.awaitTerminalEvent()
       testSubscriber.assertValues("Macoscope", "Android Apps")
}

Finally looking nice! The test code is focused only on requirements and the amount of boilerplate is reduced to a minimum.

Readable Assertions

When running unit testing, it is very important to quickly figure out why a given test has failed. Here, I am simulating an error by replacing the tested observable with Observable.error.

def 'test subscriber assertion output'() {
   setup:
       TestSubscriber<String> testSubscriber = new TestSubscriber<>()
   when:
       Observable.error(new IllegalStateException())
               .delay(1000, MILLISECONDS)
               .subscribe(testSubscriber);
   then:
       testSubscriber.awaitTerminalEvent()
       testSubscriber.assertValues("Macoscope", "Android Apps")
}

Let’s look at the output of the failing assertion and try to find the error returned by observable.

java.lang.AssertionError: Number of items does not match. Provided: 2  Actual: 0
    at rx.observers.TestObserver.assertReceivedOnNext(TestObserver.java:116)
    at rx.observers.TestSubscriber.assertReceivedOnNext(TestSubscriber.java:274)
    at rx.observers.TestSubscriber.assertValues(TestSubscriber.java:529)
    at com.macoscope.unittesting.rxjava.BlogSpec.test subscriber(BlogSpec.groovy:84)

Well, it turns out the output is not very informative. We can figure out which assertion has failed based on the line number in the stack trace (BlogSpec.groovy:84) and that no events with the result were received (Actual: 0). But there is no information as to why it has happened. It would be much better if the test failed with an error cause in the stacktrace after an unexpected error occured. This, in turn, can be improved by adding an additional assertNoErrors assertion.

def 'test subscriber with assertion failure'() {
   setup:
       TestSubscriber<String> testSubscriber = new TestSubscriber<>()
   when:
       Observable.error(new IllegalStateException())
               .delay(1000, MILLISECONDS)
               .subscribe(testSubscriber);
   then:
       testSubscriber.awaitTerminalEvent()
       testSubscriber.assertNoErrors() //redundant but improves assertion message
       testSubscriber.assertValues("Macoscope", "Android Apps")
}

After adding the redundant assertion the code is less focused on testing requirements. Now, let’s have a look at the test assertion results.

java.lang.AssertionError: Unexpected onError events: 1
        at rx.observers.TestSubscriber.assertNoErrors(TestSubscriber.java:308)
        at com.macoscope.unittesting.rxjava.BlogSpec.test subscriber with assertion failure(BlogSpec.groovy:96)
Caused by: java.lang.IllegalStateException: custom error message
        at com.macoscope.unittesting.rxjava.BlogSpec.test subscriber with assertion failure(BlogSpec.groovy:91)

Now, the reason for failure is quite clear. We can see that the test failed due to an error returned by the observable with the reason java.lang.IllegalStateException. Without assertNoErrors, I often found myself wondering why a given test has failed and wasting a lot of precious time looking for the causes of my problems.

EDIT
As of version 1.1.6 of RxJava assertion message is greatly improved and calling redundant assertNoErrors in not longer needed.

Testing RxJava Errors with TestScheduler

In case we want to test errors returned by the observable, it can also be done with the help of TestSubscriber.

def 'test subscriber with error'() {
   setup:
       TestSubscriber<String> testSubscriber = new TestSubscriber<>()
   when:
       Observable.error(new IllegalStateException("custom error message"))
               .delay(1000, MILLISECONDS)
               .subscribe(testSubscriber);
   then:
       testSubscriber.awaitTerminalEvent()
       Exception exception = testSubscriber.getOnErrorEvents().first()
       exception instanceof IllegalStateException
       exception.getMessage() == "custom error message"
}

In this test, I retrieve the first error and check its type and content. In this case, I employ a different method, getOnErrorEvents, that returns all errors and then utilizes Spock assertions to verify the results. This approach is much more flexible and allows me to use test runner tools.

TestScheduler: the Time Machine for Tests

All the approaches described above wait for the stream to finish, which can take a significant amount of time. In our case, it is at least 1 second per test, which is a lot of time for a unit test. Well, the RxJava world has a time machine! We can go forward in time by a fixed period with help of TestScheduler. The Delay operator accepts the scheduler as a last parameter, so we can pass TestScheduler to it and then travel in time. In the then: block, I employ the advanceTimeBy method and advance in time by 1 second.

def 'test subscriber with test scheduler'() {
   setup:
       TestSubscriber<String> testSubscriber = new TestSubscriber<>()
       TestScheduler testScheduler = new TestScheduler()
   when:
       Observable.just("Macoscope", "Android Apps")
               .delay(1000, MILLISECONDS, testScheduler)
               .subscribe(testSubscriber);
   then:
       testScheduler.advanceTimeBy(1000, MILLISECONDS)
       testSubscriber.assertValues("Macoscope", "Android Apps")
}

This way, our test is super fast and takes just a couple of milliseconds. It is a huge improvement over the previous test runs, which took more then 1 second each.

test results

You may think that this is some sort of sorcery, but the reality behind the tool is simple: it instantly triggers jobs scheduled for a given period. It doesn’t change the system clock or make those long-running tasks go faster. CPU-intensive tasks will still take the same amount of time. Also, you have to provide a way to inject schedulers into your production code. However, if certain conditions are met, using this tool will dramatically speed up your tests.

Conclusion

I find the TestSubscriber approach to be the cleanest and easiest to understand, it allows us to focus solely on our requirements and remove the majority of the superfluous ceremony. I encourage you to look at other helper methods provided by this tool and to use it as your default method of testing RxJava observables. Furthermore, TestSubscriber provides access to events that were emitted by the observable, allowing you to use your test tool of choice to precisely verify results. If you have an option to pass the scheduler to your production code, then you should also make use of TestScheduler, which can make your unit tests super fast. You can find all the examples presented in this article on our GitHub.


Got inspired? E-mail us and we’ll get in touch to find out how our design and development services can drive business value for you.


Related resources

http://fedepaol.github.io/blog/2015/09/13/testing-rxjava-observables-subscriptions/
https://labs.ribot.co.uk/unit-testing-rxjava-6e9540d4a329
http://blog.danlew.net/2014/09/15/grokking-rxjava-part-1/
https://caster.io/episodes/rxjava-for-android-developers/ https://medium.com/azimolabs/testing-rx-code-7918d7ee1680/