Redux-observable: How I test Async actions

Created on 14 Feb 2017  路  5Comments  路  Source: redux-observable/redux-observable

First:
I insert custom middleware into Redux. All this does is look for a Callback for a specific Action.Type.

    var callbacksAfterActionType = {};
    function reduxTestMiddleware() {
        return (next) =>
            (action) => {
                let retval;
                try {
                    retval = next(action);
                    if (callbacksAfterActionType[action.type]) {
                        let cb = callbacksAfterActionType[action.type];
                        delete callbacksAfterActionType[action.type];
                        cb();
                    }
                } catch (ex) {
                    console.error(action, ex);
                }

                return retval;
            }
    }

When I create the Redux Store, I insert the Epic middle ware, and the Test Middleware.

let reduxStore = 
        createStore(rootReducer, applyMiddleware(createEpicMiddleware(rootEpic),reduxTestMiddleware));

Then I use this helper function to dispatch [Action]'s and wait for a specific Action.

    function dispatchAndReceiveActions(sendAction, resolveActionType){
        return new Promise((resolve, reject) => {
            callbacksAfterActionType[resolveActionType] = resolve;
            if(Array.isArray(sendAction)) {
                sendAction.forEach((action) => {
                    store().dispatch(action)
                });
            } else {
                store().dispatch(sendAction);
            }
        });
    }

In my Tests, this is how I use it. It sends two actions, SELECT_DEVICE, and DISCONNECT at the same time, and waits for a DISCONNECT_COMPLETE action to be sent to the redux store. You can dispatch One-or-More actions, and wait for ONE action.

it('device connection can be cancelled before being fully established', () => {
    return dispatchAndReceiveActions([
                {type: "SELECT_DEVICE", device: device},
                {type: "DISCONNECT", device: device}
            ], "DISCONNECT_COMPLETE").then(() => {
                expect(store().getState().device).toBe(null);
            })
});

It would be nice to turn this into Observable, however Promises work just fine in this case.

Most helpful comment

@connected-mgosbee ohh that makes more sense.

It seems generally good for many use cases, but won't work in all of them--particularly ones that redux-observable is commonly used for, where multiple actions are emitted over time.

Lately I've been recommending people test the epics directly, without a redux store, which so far has had positive responses and works well for me.

// api.js
// your API call helpers
import { ajax } from 'rxjs/observable/dom/ajax';

export const fetchSomething = id =>
  ajax.getJSON(`/somethings/${id}`);

// the epic

export const somethingEpic = (action$, store, { api }) =>
  action$.ofType(SOMETHING)
    .mergeMap(action =>
      api.fetchSomething(action.id)
        .map(response => somethingFulfilled(response))
    );

// inject your dependencies into all of your epics
// when making the root epic 

import * as api from './api'

export const rootEpic = (...args) =>
  combineEpics(somethingEpic, anotherEpic)(...args, { api });

// later to test, just mock the dependencies

const action$ = ActionsObservable.of({ type: SOMETHING, id: '123' });
const response = { id: '123', something: 'example' };
const api = { fetchSomething: id => Observable.of(response) };

somethingEpic(action$, null, { api })
  .toArray()
  .subscribe(actions => {
    assertDeepEqual(actions, [
      somethingFulfilled(response)
    ]);
  });

All 5 comments

Hey, I'm not sure what you're asking?

Not really asking--- would like to just make a helpful post. Unsure of where to put it. Looking for input or any issues/improvements you may see

@connected-mgosbee ohh that makes more sense.

It seems generally good for many use cases, but won't work in all of them--particularly ones that redux-observable is commonly used for, where multiple actions are emitted over time.

Lately I've been recommending people test the epics directly, without a redux store, which so far has had positive responses and works well for me.

// api.js
// your API call helpers
import { ajax } from 'rxjs/observable/dom/ajax';

export const fetchSomething = id =>
  ajax.getJSON(`/somethings/${id}`);

// the epic

export const somethingEpic = (action$, store, { api }) =>
  action$.ofType(SOMETHING)
    .mergeMap(action =>
      api.fetchSomething(action.id)
        .map(response => somethingFulfilled(response))
    );

// inject your dependencies into all of your epics
// when making the root epic 

import * as api from './api'

export const rootEpic = (...args) =>
  combineEpics(somethingEpic, anotherEpic)(...args, { api });

// later to test, just mock the dependencies

const action$ = ActionsObservable.of({ type: SOMETHING, id: '123' });
const response = { id: '123', something: 'example' };
const api = { fetchSomething: id => Observable.of(response) };

somethingEpic(action$, null, { api })
  .toArray()
  .subscribe(actions => {
    assertDeepEqual(actions, [
      somethingFulfilled(response)
    ]);
  });

@connected-mgosbee I also tend to use the very same approach described by @jayphelps and found out to be very easy to test this kind of stuff.

Closing because we have other tickets that are tracking the needed documentation updates.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

MikeRyanDev picture MikeRyanDev  路  3Comments

winkerVSbecks picture winkerVSbecks  路  4Comments

ncuillery picture ncuillery  路  3Comments

fedbalves picture fedbalves  路  4Comments

ericis picture ericis  路  3Comments