There is very little material on unit testing reactive JavaScript code. This blog post tries to describe many little problems I have encountered with a few solutions.
- The problem
- Refactor for testability
- Our first unit test
- Catching errors in the unit test
- Testing with Ava
- Verifying data vs verifying timestamps
- Caution: functions vs arrow expressions
- Build test streams
- Helper test observer
- Testing hot observables
- Virtual timing for faster tests
- Summary
- Additional information
The problem
Give the following simple Rx example below, how would you unit test it?
1 | // even-numbers.js |
We can run the code and see the result to confirm it works
$ node even-numbers.js
even number 2
even number 4
even number 6
all done
How do we unit test the stream of even numbers?
Refactor for testability
In the above example, the code that sets up the stream of even numbers is mixed with the code that actually subscribes to the stream and prints the information to the console. While we could unit test the command line output, this would be far from ideal for unit tests.
Let us split the stream creation from the printing code. We can create the
evenNumbers_
stream in the even-numbers.js
and subscribe to it in the file index.js
1 | const Rx = require('rx') |
1 | const evenNumbers_ = require('./even-numbers') |
This runs just fine
1 | $ node index.js |
Our first unit test
We can write a few simple unit tests, using Mocha - my favorite unit testing framework. Let us confirm that we get an Observable instance
1 | const la = require('lazy-ass') |
Let us make sure the sequence finishes after a few numbers. Since the test is asynchronous, we will use a callback to let Mocha runtime know when the stream has finished.
1 | const la = require('lazy-ass') |
We used a dummy function noop
when subscribing and ignored everything but the "stream completed"
event.
Let us confirm that we get 3 even numbers from this stream. We can increment the count on each number received and can check the total when the stream completes
1 | it('has 3 numbers', (done) => { |
Catching errors in the unit test
Let us also confirm that there were no error events. For example, an error could happen somewhere inside the stream
1 | // even-numbers.js |
Our test should fail with useful information if there is an error event. Let us rethrow an Error instance if the stream has an error.
1 | // spec.js |
If we run this unit test, we get a long stack trace
1 | Error oh no |
The long stack is a little bit too much. Luckily we can enable the long stack support. This will make the reactive code slower, thus we do this in the unit test.
1 | const Rx = require('rx') |
1 | Error: oh no |
The stack is much more helpful. Notice we had to load Rx
in the unit test before
the production code has loaded it. This is because the Node runtime caches loaded modules.
We must enable the long stack support before even-numbers.js
uses Rx to configure the
stream.
Testing with Ava
In addition to Mocha, I have tried testing reactive streams using Ava. Mocha is nice because it understands promises natively, but Ava can understand async unit tests that return promises, generators, and even observables! For example, to make sure our stream only returns even numbers, we could just return the observable
1 | import test from 'ava' |
We even made sure in our tests to check how many even numbers were processed using t.plan(n)
function that sets the expected number of assertions in the unit test. Every time t.true()
runs, the assertion counter is incremented. At the end of the test the two numbers are compared,
making sure our test took the execution path we expected.
Unfortunately, Ava understands ES6 Observables and not as they are implemented in RxJS v4. Thus I had to modify the code a little to use RxJs v5. The change was simple in our example
npm install --save rxjs
The only change in the even-numbers.js
was the require name
1 | const Rx = require('rxjs/Rx') |
If we had more complicated code, the migration from Rx v4 to v5 would be more complicated
Verifying data vs verifying timestamps
A reactive stream has really two types of information
- The data itself (including the order). For example, even numbers stream has
the following data
2, 4, 6
. - The timing information when the events arrive. For example, the following stream has delays between the numbers.
1 | const array = [1, 2, 3, 4, 5, 6] |
If we use this stream and print the time intervals (that measure time since the last event) we get the following
1 | $ node index.js |
In marble terms, our stream looks like this.
1 | |---1-2-3-4-5-6-X-> |
Notice the delay
operator delays an event from the start of the observable stream, which
for our purposes is timestamp 0 of the program.
How can we check the values (2, 4, 6
) and when the events arrive (1 second apart in this case)?
Again, we must start with refactoring to separate the two aspects. This will make testing easier.
For example, we can create the delayed number stream from the even number stream.
1 | // even-numbers.js |
We will test the simple numbers list in the first test
1 | const evenNumbers_ = require('./even-numbers') |
We can also verify the delayed numbers are generated with expected time intervals. First, we need to tell Mocha to wait longer for the unit test to finish, and second, we need to grab the timestamps. I will use a couple of extra helper functions to round the timestamps to seconds
1 | const delayedNumbers_ = require('./even-delays') |
We expect the timestamps
array in the second test to have values [2, 2, 2]
at the
end of the test.
Caution: functions vs arrow expressions
Note we have used describe('...', function () {})
callback in the above example
rather than the arrow function describe('...', () => {})
because the Mocha runtime
uses suite and test context objects under the hood.
1 | describe('delayed numbers', function () { |
Be careful when binding the context to the outer scope in Mocha, and in RxJS. For example, the following are different ways to create an observer are different
1 | // works |
Always use strict
in your code to avoid accidentally using the global object as the scope
for the fat arrows, and try to stick to explicit functions.
I have been burnt by this error in both MochaJS and RxJS, so hope this saves you some time.
Build test streams
In the above example test, we have switched from stream composition (evenNumbers_
to delayedNumbers_
)
to functional composition (msToSeconds(roundHundred(x.interval))
expression)
1 | const roundHundred = x => Math.round(x/100)*100 |
Since we already have reactive streams, we can easily avoid additional functional pipelining,
building up test streams instead. In this case, we want to extend the delayedNumers
stream
and grab just the timestamps, creating a stream specifically for testing.
1 | it('has the right timestamps', (done) => { |
Basically, we transform each event, massaging the data, and buffering until we get 3 timestamps
that we send to the verifyTimestamps
. I prefer this form because it cleanly separates
the data check (first .subscribe
callback) from the stream complete event (plain done
function).
On the other hand, we no longer make sure that we actually verified the timestamps.
For example, if the underlying stream never invokes onNext
(that is, no events every happen),
then the verifyTimestamps
callback never executes, and the test just finishes.
Mocha (unlike Ava) does not support assertion counting (because the test runtime is really decoupled from any particular assertion library). Thus we cannot easily set the expected number of assertions and catch the lack of events.
This is a great opportunity to build a tiny helper for testing streams.
Helper test observer
We have subscribed to test streams using 3 separate callback functions. RxJS has a second form of subscribing, passing a single observer instance instead.
1 | const observer = Rx.Observer.create( |
This subscribe format is convenient for creating an observer that makes sure its onNext
has been executed, for example. Here is one implementation
1 | function makeTestObserver(onNext, onError, onComplete) { |
Now our unit tests can be dependable without much boilerplate
1 | it('does everything using test Observer', (done) => { |
Less boilerplate - simpler testing.
Testing hot observables
So far we had simple "cold observables". Every subscriber got the same sequence of numbers, even if there were multiple subscribers.
1 | evenNumbers_.subscribe( |
But some observables, like current time stamps or mouse clicks stream, are "hot" - they send "live" events, and if a subscriber missed them, those events are gone and will not be repeated. Most common way to miss events is to subscribe to the events after it has already started broadcasting.
In RxJS we can create "hot" even numbers stream from a cold one using .publish()
method call.
The new hotNumbers_
stream will start broad casting after we execute .connect()
call.
1 | // hot-numbers.js |
The stream hotNumbers_
starts right away, and if we subscribe to it from another file - well,
it will be too late
1 | // use-hot-numbers.js |
Once the hot stream has completed, it stays completed and any subscriber gets just the "done"
callback executed. This is similar to adding another .then(cb)
step to a Promise that has been
fulfilled - the callback will be executed with the value, but the initial promise function will not
be run again.
Hot observables make testing more complex. We must ensure 2 things
- We connect to the hot observable before it starts broadcasting.
- We recreate a hot observable from scratch in each unit test, because the events are not going to be repeated.
I would refactor the production code so that each module returns either a cold observable
or a function returning a new hot observable. For example, the even-numbers.js
returns
a cold observable, and hot-numbers.js
returns a hot one.
1 | // even-numbers.js - returns COLD observable |
The production code that wants to use hotNumbers_
instance would call the function
and the call the .connect()
method to start pushing the events
1 | const hot_ = makeHotNumbers() |
The unit tests can make a fresh hot observable and start events, when needed.
1 | const makeEvenNumbers = require('./hot-numbers') |
The separation between the construction code and the actual execution is similar to
IO Monad concept and moving side effects into specific parts of the code
(like the application's periphery or unit tests). In RxJS case, the construction of hot
observable is pure - we can call the makeEvenNumbers()
as many times as we want with the
same result - a fresh observable. The side effects are only possible when we execute
hot_.connect()
- the changing internal state of the observable being the side effect.
I have described a very similar concept for a single async action wrapped inside a Task object. Read the blog post Difference between a Promise and a Task. to see the same tight boundary between the pure code construction and the side-effects during the execution.
Virtual timing for faster tests
We have a problem with delayed numbers stream - it uses real world timer, taking too long! Waiting 6 seconds in our test just to confirm that 3 even numbers appear 2 seconds apart is taking too long. Can we use a virtual timer in our unit tests instead?
Schedulers
Take a simple source of events, for example a timer generating timestamps every second.
To make sure it terminates, we will use .take(3)
method call to only grab first 3 events
1 | const Rx = require('rx') |
The timer observable emits an event, then waits 1 second, emits the second event, waits 1 second, etc. The waiting period is controled by a scheduler. Most Rx methods take a scheduler instance as the last argument, including Rx.Observable.timer. There are [several schedulers] included in Rx, but for testing, we can use Rx.TestScheduler. Here is how to use it.
First, if we just pass an instance to the timer
method nothing happens - the program
finishes without any events!
1 | const Rx = require('rx') |
This is because the test scheduler needs explicit events from us, including when to execute them. We will schedule 3 "onNext" events and then 1 "onComplete".
1 | const timerScheduler = new Rx.TestScheduler() |
We used startScheduler()
method that returns an observable of Rx.ReactiveTest
events.
Now our timer generates events very quickly (in real time)
1 | $ node timer-test.js |
Rx.TestScheduler example
Since we need to pass a scheduler when creating an Observable if we want to speed up the time, we have to refactor even cold observables into factory methods. For example, let us make a factory method to delay even numbers
1 | const Rx = require('rx') |
The production code does not need to pass anything; the default scheduler will be
used if the argument sheduler
is undefined.
1 | delayedNumbers_ = makeDelayedNumbers() |
Inside our unit test, we will construct a test scheduler and we will make the test run fast
1 | const Rx = require('rx') |
Running this unit test is much faster
1 | > mocha virtual-timing-spec.js |
Of course, the extra test code complexity is a huge trade off in this case.
Summary
When unit testing reactive streams
- Use
done
Mocha callback to let the testing runtime continue - Turn on the long stack support before the production code loads
- Do not forget to verify the test path by confirming the number of assertions, or by throwing an exception if a stream goes through a different path
- Build additional streams with extracted test data for easy validation
- If you already use Rx v5 try Ava test runner
- There is plenty of room for writing your own little helper functions to remove the stream testing boilerplate
- If the tests are really really slow, consider using
Rx.TestScheduler
to drive the events on virtual time
Additional information
- RxJs Testing in Real World Applications really good examples testing real world code and using schedulers.
- Testing your Rx application - mostly talks about fake timers and long stack support.
- How to Debug RxJS code
- Cold and hot observables.