Testing Redux-Observable Epics

Here’s a way to test a redux-observable epic that performs an ajax call.

This simplified TypeScript example has three actions: USER_LOAD_REQUEST is the result of a request to load a user, USER_LOAD_RESULT indicates a successful response, and USER_LOAD_ERROR holds an ajax error.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import {ActionsObservable} from "redux-observable";
import {AjaxError} from "rxjs/Rx";
import "rxjs/Rx";

export const USER_LOAD_REQUEST = "example/USER_LOAD_REQUEST";

export const USER_LOAD_RESULT = "example/USER_LOAD_RESULT";

export const USER_LOAD_ERROR = "example/USER_LOAD_ERROR";

export interface IUserResult {
    id: string;
    name: string;
}

export interface ICustomAjaxError {
    type: string;
    message: string;
}

export const loadUser = (userid: string) => ({
    type: USER_LOAD_REQUEST,
    userid
});

export const loadUserResult = (results: IUserResult) => ({
    type: USER_LOAD_RESULT,
    results
});

export const loadFailure = (message: string): ICustomAjaxError => ({
    type: USER_LOAD_ERROR,
    message
});


export const loadUserEpic = (action$: ActionsObservable<any>, store, {getJSON}) => {
    return action$.ofType(USER_LOAD_REQUEST)
        .do(() => console.log("Locating User ...")) // debugging
        .mergeMap(action =>
            getJSON(`%PUBLIC_URL%/api/users`)
                .map(response => loadUserResult(response as any))
                .catch((error: AjaxError): ActionsObservable<ICustomAjaxError> =>
                    ActionsObservable.of(loadFailure(
                        `An error occurred: ${error.message}`
                    )))
        );

};

export default loadUserEpic;

Testing is straightforward with ReduxObservable’s dependency injection parameter. In this case, I’m injecting the ajax.getJSON property into the epic.

Mocking the AJAX call involves either wrapping a plain JS object or a plain JS Error in an Observable.

import { ActionsObservable } from "redux-observable";
import "rxjs/Rx";
import { Observable } from "rxjs/Observable";

import * as example from "./exampleEpic";

const successResult: example.IUserResult = {
    id: "123",
    name: "Test User"
};

describe("loadUserEpic", () => {

    // done: see https://facebook.github.io/jest/docs/asynchronous.html

    it("dispatches a result action when the user is loaded", (done) => {

        const dependencies = {
            getJSON: url => Observable.of(successResult)
        };

        const action$ = ActionsObservable.of(example.loadUser(successResult.id));
        const expectedOutputActions = example.loadUserResult(successResult);

        const result = example.loadUserEpic(action$, null, dependencies).subscribe(actionReceived => {
            expect((actionReceived as any).type).toBe(expectedOutputActions.type);
            done();
        });

    });

    it("dispatches an error action when ajax fails", (done) => {
        
        const errorMessage = "Failed Ajax Call";
        
        const dependencies = {
            getJSON: url => Observable.throw(new Error(errorMessage))
        };

        const action$ = ActionsObservable.of(example.loadUser(successResult.id));
        const expectedOutputActions = example.loadFailure(`An error occurred: ${errorMessage}`);

        const result = example.loadUserEpic(action$, null, dependencies).subscribe(actionReceived => {
            expect((actionReceived as any).type).toBe(expectedOutputActions.type);
            expect((actionReceived as any).message).toBe(
                expectedOutputActions.message);
            done();
        });

    });

});

Note the done parameter—this is a way of signaling to jest that an async call has completed.