Creating unit tests for redux-observable with Marble diagrams

Dmitry Martynov
4 min readDec 14, 2017

--

Nowadays WEB apps become more and more complicated, common web developer has to use a lot of tools to create great application. Fortunately, our community has a lot of great developers who create tons of cool tools that help us to develop easily. But sometimes, that tools are very difficult to understand and when you start using them you can’t believe that they will help you, first time you think that you will probably suffer while using such tools as RX.js, Redux, Redux-observable etc. When I was working on the Pitch app of DataRobot[Labs], I faced a task where user was able to navigate through lots of pages, when user navigates to the next page the data for the previous page should stop loading. BTW about the app. It uses DataRobot product on a backend to predict the kind of the next pitch which pitcher throws. The app is based on MLB dataset and DataRobot prediction models. If you like baseball try using that app while watching a live game, it should be interesting. :)

I’d heard about RX.js then, so I decided that it would fit for that kind of tasks very nicely, and I decided to use it. And yes, first time I was suffering, but then, I was suffering even more when I started creating unit tests for all that code I had written. I figured out that Google couldn’t help me to find good article about RX.js testing. I found lots of small pieces of documentation in different resources and at least my code was covered by unit tests. In this article I will combine all that information and explain how to create unit tests for RX.js. I will not explain how the RX and redux-observable works. If you are reading this article you probably have to know about all those tools, so I will cover only unit tests part.

Epic

Let’s skip all that unnecessary things that I had learned when I was digging WEB for all the RX-related docs, let’s just create a redux-observable epic.

  1. It calls only if redux-observable middleware receives action with LOAD_GAMES type;
  2. It delays call according to debounce time.
  3. It merges two actions SET_GAMES_LOADING and RECEIVE_GAMES.
  4. It repeats the ajax call according to timeout (Note: It repeats only the ajax call. It doesn’t repeat SET_GAMES_LOADING action, it polls for games without letting user know about this, so user will not see loading indicator, it will see it only the first time).
  5. It retries to load games if server or some else error occurs.
  6. It continues to poll server till LOAD_GAMES_CANCEL comes.

Looks good and simple and it works.

Testing

Let’s start testing that code. You probably know that testing asynchronous code is a big pain in the ass. So, now it’s not, RX.js has great approach to test async code with Marble Diagrams. Let’s create marble diagram for next case.

loadGames(action$, null, ts, 10, 10);

When this epic is run by Redux-observable middleware. $action is an Instance of ActionsObservable class which extends Rx.Observable. In this article we are not going to test all the Redux flow, so we have to create our own instance of $action. To do that we have to create TestScheduler.

const ts = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});

So, as we can see in the docs:

TestScheduler is a virtual time scheduler used for testing applications and libraries built using Reactive Extensions.

Yes, TestScheduler is going to help us to get rid from the asynchrony. So let’s create our $action.

const action$ = new ActionsObservable(ts.createHotObservable(inputMarble, inputValues));

I use hot observable because in real redux flow our actions are initialized immediately and wait for correct action type to run.

Let’s create inputMarble.

const inputMarble = 'a-----b';

It means that we emit a at 0 frame, then we don’t do anything for 10–50 frames and emit b at 60 frame. To make our epic work we have to create correct values to emit.

const inputValues = {
a: { type: 'LOAD_GAMES', payload: { date: '2010-10-10' } },
b: { type: 'LOAD_GAMES_CANCEL', payload: {} },
};

inputValues.a will substitute a marble and the same for b. And now we can call our epic like:

const outputAction = loadGames(action$, null, ts, 10, 10);

TestScheduler mocks the time but it doesn’t mock the ajax. So let’s mock it with Jest.

import { ajax } from 'rxjs/observable/dom/ajax';
jest.mock('rxjs/observable/dom/ajax', () => ({
ajax: jest.fn(),
}));

Now ajax is a simple Jest spy function. Then we have to add an implementation and the value is returned by that function:

const mockImplementation = () => {
let callCount = 0;
const responses = [
null,
[{
gameId: 0,
}], [{
gameId: 0,
}], [{
gameId: 0,
}], [{
gameId: 0,
}],
];
return Rx.Observable.create((observer) => {
if (callCount === 0) {
observer.next(null);
} else {
observer.next({ response: responses[callCount] });
}
callCount += 1;
observer.complete();
});
};
const ajaxMock = jest.fn();

ajaxMock.mockReturnValue(mockImplementation());
ajax.mockImplementation(ajaxMock);

It works pretty easy, ajax has some index counter for calls and it emulates server response according to call number. Then we have to create expect observable:

ts.expectObservable(outputAction).toBe(expectedMarble, expectedValues);

And expectedMarble and expectedValues:

const expectedMarble = '-abbbb------';
const expectedValues = {
a: {
type: 'SET_GAMES_LOADING',
payload: { date: '2010-10-10' },
},
b: {
type: 'RECEIVE_GAMES',
payload: {
date: '2010-10-10',
items: [{
gameId: 0,
}],
},
},
};

Then we just need to flush the TestScheduler:

ts.flush();

And we can optionally add some asserts for ajax mock:

expect(ajaxMock).toHaveBeenCalledTimes(1);
expect(ajaxMock).toHaveBeenCalledWith({
url: '../games/date/2010-10-10',
responseType: 'json',
});

That’s it. Thank you for reading. Here is a full code sample for that test we have just created:

https://gist.github.com/reptiloit/7bdfaa5bf34f086653c1df02b75fc9e1

https://gist.github.com/reptiloit/cf16480e7df05d1f7ed3fa6a717df9ce

--

--