Redux: Document a simpler way to test async action creators

Created on 11 May 2016  路  12Comments  路  Source: reduxjs/redux

Since Redux Thunk 2.1.0, we can do something like this:

import * as api from '../api'

export default function configureStore(mocks) {
  return createStore(
    reducer,
    applyMiddleware(thunk.withExtraArgument({
      api,
      // can also pass other modules you need in ACs and want to mock in tests
    }))
  );
}

Your action creators now don鈥檛 depend on those modules:

export function getUser(id) {
  return (dispatch, getState, { api }) =>
    api.getUser(id).then(() => dispatch({ ... }))
  }
}

In tests, you should be able to call them directly with spies and pass your mocks as the third argument.
Looks perfectly testable to me. Seems like there is need for a third-party library.

What am I missing?

cc @arnaudbenard

docs feedback wanted

Most helpful comment

Is there any update in the docs for this?

All 12 comments

We do something similar in our app. Instead of using redux-thunk, we have our own middleware that passes a single object with dispatch, getState, and fetch.

Have you considered passing the store as a single param to the thunk? I think it's nice to be able to keep all those stateful bits of your app glued to a single object.

You can use store enhancers to do barebones ghetto DI for the handful of things you need to inject. In our app, there's three such things: fetch (window.fetch in browser, fakeFetch in tests, wrapped node-fetch in server if we ever do SSR), history (result of createHistory in browser, result of createMemoryHistory in tests), and our search util (worker search util in browser, sync search util in tests).

For example, a store enhancer to add store.fetch. It's easy to test, and it's easy to setup for server-side rendering. You can pass isomorphic-fetch as a param, wrapped in the server so it'll prefix all requests that start with / with the full domain. To make it nicer in the test environment, you can add something like store.queueResponse / store.addResponse or something along those lines. I wrote a utility that's kinda like that for our app. The nice thing about fetch is that you can create real responses by using new Response({ ... }), so you mock as little code as possible.

In the case of history, the reason for having it running with the store is so you can run tests that interact with the router without having to render any React code. (Calling match after each location update.)

For the test environment, we ended up making a helper function: createTestStore to pass in a fake fetch, memory history, etc.

Have you considered passing the store as a single param to the thunk?

We definitely won鈥檛 break existing API because it鈥檚 too widely used at this point. It鈥檚 just not worth it.

Great addition to the redux-thunk API. It looks testable to me. 馃憤

Is there any update in the docs for this?

@gaearon if there are many separate api modules in a large application, would you still pass all of them into the middleware to be accessible anywhere an action-creator is used?

@dannyshaw even with this possibility of injection into action creator function, it would be hard to inject a lot of dependencies.

In our team, we have ended up created wrappers for all API calls and just mocking them per test. That allows modeling certain testing scenario.

Example:
We are using github/fetch for HTTP requests.
We have created api module which wraps fetch methods and manage headers, etc.
So we have all HTTP method wrappers (get, post, put, delete).
Then we have api wrapper per resource like userApi, todosApi, etc.
The purpose of those wrappers is to handle request composition (urls, etc) and creating abstraction level which is easy to mock.
And in tests we are just mocking all of the needed resource wrappers calls:

spyOn(userApi, 'get').andReturn(result1);
spyOn(todosApi, 'get').andReturn(result2);

Also, we are exporting factory function for mocks from async actions tests, so we are able to mock all dependencies for async action creators in complex async action creator:

spyOn(userApi, 'get').andReturn(result1);

// This is a dependency of Action B, so it should be imported from tests of action B
spyOn(todosApi, 'get').andReturn(result2);

expect(actionA()).toDispatch([
  { type: action_a_start },
  { type: action_a_success },
  actionB()
], done);

function actionA() {
  return dispatch => {
    dispatch(actionAStart());
    return user.get().then(response => {
        dispatch(actionAFinish(response));
        dispatch(actionB());
      });
    };
}

function actionB() {
  return dispatch => {
    dispatch(actionBStart());
    return getTodos().then(response => {
        dispatch(actionBFinish(response));
      });
    };
}

@gaearon can you please describe more in details what kind of functionality would you expect from that third-party library?

Finally, what is the conclusion @gaearon ? @dmitry-zaets, @dannyshaw

I think the conclusion is that it's definitely possible to inject dependencies into thunks via the extraArgument approach, but if you've got a lot of stuff you're trying to inject, that might get annoying.

If anyone does want to add a section to our testing docs that mentions this, please comment here and let me know. Until then, closing due to inactivity.

@markerikson I'm working on testing documentation for redux, what would you like to learn/know?

@arnaudbenard : I don't have any specifics I personally want added atm, but I also haven't reviewed our docs on testing lately.

What updates are you working on?

@arnaudbenard how about adding something about how to handle the tests when the fetch is not made in the action creator, but rather in some api file. e.g. How would i handle the situation when instead of return fetch('http://example.com/todos').then.... etc
we have call to external funciton, API.todos().then..... etc

Was this page helpful?
0 / 5 - 0 ratings