Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

testing async store.dispatch #546

Closed
kswope opened this issue Aug 16, 2015 · 25 comments
Closed

testing async store.dispatch #546

kswope opened this issue Aug 16, 2015 · 25 comments

Comments

@kswope
Copy link

kswope commented Aug 16, 2015

Maybe I'm overlooking something but I'm finding it impossible to test store.dispatch with an async action without a hacky timer solution. In the below example actions.authenticate contains an ajax call. The ajax call is mocked but its still in an async situation. I'm using redux-thunk middlewear. testTimeout() is just setTimeout with a 0 second delay.

Yes I know the design of redux allows the separate testing of actions, reducers, etc, but testing store.dispatch itself is a lot of bang for the buck because it ties store, reducers, actions and middlewear together.

      test( 'success', (done) => {                                                                                                                  

         fauxJaxResponse( '/authenticate', { 'the': 'token' } )                                                                                      

         store.dispatch( actions.authenticate( 'myname', 'mypass' ) )                                                                                

         testTimeout(()=>{                                                                                                                           
           // store.getState() asserts here                                                                                                                           
           done()                                                                                                                                    
         })                                                                                                                                          

       }) 

I could test actions and reducers together (below) bypassing store.dispatch but its incomplete because its bypassing any middlewear. Note that actions.authenticate() returns a thunk that calls mockDispatch when ready

test( 'success', (done) => {                                                                                                                  


  fauxJaxResponse( '/authenticate', { 'the': 'token' } )                                                                                            

   var mockDispatch = function( action ) {                                                                                                           
     state = reducer(action)                                                                                                                         
     // state asserts here                                                                                                                           
     done()                                                                                                                                          
   }                                                                                                                                                 

   actions.authenticate()( mockDispatch )  

})

I almost want store.dispatch() to take a callback as a second param, to be called when the dispatch is complete, but that's weird right, and only useful in testing?

@taylorhakes
Copy link
Contributor

You can have a callback by using store.subscribe. It is used by react components to get state updates.

var unsubscribe = store.subscribe(function() {
  expect(store.getState()).toEqual(...);
  done();
});

// Some async dispatch
actions.authenticate()( store.dispatch )  ;

You probably want to clean up the subscriber after the test

unsubscribe();

@kswope
Copy link
Author

kswope commented Aug 17, 2015

I didn't even come to mind within the context of testing, that's a great solution. I'm assuming it fires after a dispatch and not at some arbitrary time. Thanks!

@gaearon
Copy link
Contributor

gaearon commented Aug 17, 2015

I'm reopening so somebody can contribute this to the Writing Tests recipe. Please write here if you make a PR!

@idolize
Copy link
Contributor

idolize commented Aug 25, 2015

Glad I found this issue! Was racking my brain trying to figure out something elegant here. Thanks!

@gaearon
Copy link
Contributor

gaearon commented Aug 26, 2015

Rather than this:

var unsubscribe = store.subscribe(function() {
  expect(store.getState()).toEqual(...);
  done();
});

// Some async dispatch
actions.authenticate()( store.dispatch )  ;

you can just write this:

var unsubscribe = store.subscribe(function() {
  expect(store.getState()).toEqual(...);
  done();
});

// Some async dispatch
store.dispatch(actions.authenticate());

as long as your store in tests also has the same middleware applied.

@takashi
Copy link
Contributor

takashi commented Oct 3, 2015

I'm confusing about testing async action dispatch

I'd like to test like below

function postOauthTokenRequest() {
  return {
    type: POST_OAUTH_TOKEN_REQUEST
  };
}

function postOauthTokenSuccess(body) {
  return {
    type: POST_OAUTH_TOKEN_SUCCESS,
    body
  };
}

function postOauthTokenFailure(ex) {
  return {
    type: POST_OAUTH_TOKEN_FAILURE,
    ex
  };
}

export function postOauthToken(data) {
  return dispatch => {
    dispatch(postOauthTokenRequest());
    return fetcher.post('http://localhost:3000/oauth/token', data)
      .then(json => dispatch(postOauthTokenSuccess(json.body)))
      .catch(ex => dispatch(postOauthTokenFailure(ex)));
  };
}

I'd like to test async request postOauthTokenSuccess and postOauthTokenFailure when postOauthToken is called and execute dispatching.

by using the sample test code above written by @gaearon , how can i test it?

because I have to wait dispatching postOauthTokenSuccess after postOauthTokenRequest has dispatching.

@salmanm
Copy link

salmanm commented Oct 8, 2015

@gaearon I am still not able to understand how do I test async actions and mock ajax response within them and dispatch the newAction. Once I understand, I'll make sure to raise a PR in the real-world example. Could you give some pointers?

@gaearon
Copy link
Contributor

gaearon commented Oct 8, 2015

You need some kind of way to mock fetch for tests. How you do that is up to you—it's not different from normal dependency injection and isn't specific to Redux. Once you mock fetch, you can use that mock in your tests.

@svnlto
Copy link

svnlto commented Oct 8, 2015

You need some kind of way to mock fetch for tests.

One way would be to mock responses with something like nock.

@salmanm
Copy link

salmanm commented Oct 8, 2015

Thanks. I thought it to be complicated. But if its as simple as mocking ajax request and nothing else then I did something like this. I am using superagent.

spyOn(superagent.Request.prototype, 'end').and.callFake((cb) => {
    cb(null, {key: val}); // callback with mocked response
});

@takashi
Copy link
Contributor

takashi commented Oct 8, 2015

yep i've mocked my request with nock like this

nock('http://localhost:3000/')
  .get('/oauth/token')
  .reply(200);

describe('oauth actions', () => {
  it('creates POST_OAUTH_TOKEN_SUCCESS', (done) => {
    store.subscribe(() => {
      expect(store.getState()).toEqual(...);
      done();
    });
    store.dispatch(actions. postOauthToken())
  });
});

And I'd like to test when POST_OAUTH_TOKEN_SUCCESS action that should be dispatch after POST_OAUTH_TOKEN_REQUEST dispatched.

how can i get action type inside store subscription?

sorry if i misunderstanding how to test async actions.

@gaearon
Copy link
Contributor

gaearon commented Oct 9, 2015

Folks, this is really not any different from how you'd unit test any custom methods. Not specific to Redux at all. If you need to know action type, you need to spy on store.dispatch() just like you'd normally spy on it with your unit test framework.

@gaearon
Copy link
Contributor

gaearon commented Oct 9, 2015

To give you an idea (I haven't tested though):

describe('oauth actions', () => {
  afterEach(() => {
    nock.cleanAll();
  });

  it('creates POST_OAUTH_TOKEN_REQUEST and POST_OAUTH_TOKEN_SUCCESS', (done) => {
    nock('http://localhost:3000/')
      .get('/oauth/token')
      .reply(200);

    const expectedActions = [
      { type: 'POST_OAUTH_TOKEN_REQUEST' },
      { type: 'POST_OAUTH_TOKEN_SUCCESS', body: { /* ... */ } }
    ];

    function mockDispatch(action) {
      const expectedAction = expectedActions.shift();
      expect(action).toEqual(expectedAction);

      if (!expectedActions.length) {
        done();
      }
    }

    actions.postOauthToken(mockDispatch);
  });
});

This also doesn't require you passing store and won't blow up if your reducer fails (which is irrelevant to this test).

As you can see this has nothing to do with Redux. You're just testing a function that accepts a callback.

@gaearon
Copy link
Contributor

gaearon commented Oct 9, 2015

Here's an example with API middleware from another thread:

import apiMiddleware from '../middleware/api';

const middlewares = [apiMiddleware];
/**
 * Creates a mock of Redux store with middleware.
 */
function mockStore(getState, expectedActions, onLastAction) {
  if (!Array.isArray(expectedActions)) {
    throw new Error('expectedActions should be an array of expected actions.');
  }
  if (typeof onLastAction !== 'undefined' && typeof onLastAction !== 'function') {
    throw new Error('onLastAction should either be undefined or function.');
  }

  function mockStoreWithoutMiddleware() {
    return {
      getState() {
        return typeof getState === 'function' ?
          getState() :
          getState;
      },

      dispatch(action) {
        const expectedAction = expectedActions.shift();
        expect(action).toEqual(expectedAction);
        if (onLastAction && !expectedActions.length) {
          onLastAction();
        }
        return action;
      }
    };
  }

  const mockStoreWithMiddleware = applyMiddleware(
    ...middlewares
  )(mockStoreWithoutMiddleware);

  return mockStoreWithMiddleware();
}

describe('action creators', () => {
  afterEach(() => {
    nock.cleanAll();
  });

  it('creates signup actions', (done) => {
    // mock requests you need in this test
    nock('http://localhost:3000/')
      .get('/signup')
      .reply(200);

    const expectedActions = [
      { type: 'SIGNUP_REQUEST' },
      { type: 'SIGNUP_SUCCESS', payload: { /* ... */ } }
    ];
    const mockState = {
      something: 42
    };

    const mockStore = createMockStore(mockState, expectedActions, done);
    mockStore.dispatch(signup());
  });
});

@takashi
Copy link
Contributor

takashi commented Oct 10, 2015

@gaearon thank you for describing how to test :))
and I add your way to test to docs for testing and pull request-ed. can you check this? #876

@gaearon
Copy link
Contributor

gaearon commented Oct 10, 2015

Thanks! Closing, as this is now in the docs via #878 which superseded #876.

@gaearon gaearon closed this as completed Oct 10, 2015
@damassi
Copy link

damassi commented Nov 19, 2015

I'd like to resurface this because it appears that nock is serverside only, unless I'm doing something incorrect? I'm using Webpack.

@idolize
Copy link
Contributor

idolize commented Nov 20, 2015

@damassi If you run your tests using Node.js as the runtime, mocha as the testing framework, and jsdom to mock the browser environment then you can test "client-side" code on the "server" (I'm putting these terms in quotes, because really Node.js doesn't have to be used as a "server" - here it's acting more as a headless broswer/test harness).

Thus, because you are running your tests via Node.js, you can use nock to test (jsdom will turn XMLHttpRequest AJAX calls in your code into the Node.js http equivalent, which nock will in turn mock for you).

There are plenty of other XHR mocking libraries out there too if you'd prefer to run your unit tests inside a browser instead of Node.js (see: Sinon.js).

@damassi
Copy link

damassi commented Nov 20, 2015

thanks @idolize - Yeah, I'm doing all of these things, but through Karma as the env. Very odd! I was getting errors along the lines of can't find "fs" "http" etc -- things from the node standard library. I'll re-investigate.

@thinkloop
Copy link

thinkloop commented Sep 20, 2016

Does this technique of subscribing still work when there is an expect? I currently get a timeout error if there is an expect before the done(), as though the done() never runs:

Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.

This is my code:

import { expect } from 'jest';

import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';

import { addTodo } from '../../../todos/actions/add-todo';

const mockStore = configureMockStore([thunk]);

describe('addTodo', () => {
    it('creates new todo and dispatches UPDATE_TODOS', (done) => {
        const store = mockStore({ todos: [] });

        const unsubscribe = store.subscribe(function() {
            expect('a').toEqual('a');
            done();
        });

        store.dispatch(addTodo('new description'));
    });
})

If the expect is commented out, it completes successfully.

Additionally, if not using promises, is this subscribe technique the best way to test callback oriented actions? Ex:

export function addTodo(description) {
    return (dispatch, getState) => {
        const { todos } = getState();
        const position = todos.length;

        newTodo(description, position, (err, todo) => {
            if (err) {
                console.error('newTodo:', err);
                return;
            }
            const id = todo.id;
            delete todo.id;
            dispatch({ type: UPDATE_TODOS, todos: { [id]: todo }});
        });
    };
}

It seems like it might be messy keeping track of multiple dispatches in the callback.

@lucaspiller
Copy link

I got a bit fed up of writing async actions-creator tests different from sync action-creator tests, so I wrote a simple middleware that I include in the test/mock store (inserted before redux-thunk), which adds promises all the way down:

const promisifyMiddleware = ({dispatch, getState}) => next => action => {
  return new Promise( (resolve) => resolve(next(action)) )
}

This means whether an action-creator is async or not (i.e. whether it returns a promise or not), you can test it in the same way like this:

it('dispatches something', () => {
  const expectedActions = [ { type: .. } ]

  return store.dispatch(actions.maybeDoSomethingAsync())
    .then(() => {
       expect(store.getActions()).toEqual(expectedActions)
     })
})

@reduxjs reduxjs deleted a comment from benbonnet Jun 20, 2017
@heridev
Copy link

heridev commented Aug 2, 2017

I just created a new blog post about it http://elh.mx/javascript/how-to-test-your-ajax-http-requests-for-actions-in-react-redux/, I think that @lucaspiller you need to include done something like this:

it('dispatches something', (done) => {
  const expectedActions = [ { type: .. } ]

  return store.dispatch(actions.maybeDoSomethingAsync())
    .then(() => {
       expect(store.getActions()).toEqual(expectedActions)
       done();
     })
})

Cause If you don't use it seems like it's never executed the expectation within the promise Am I wrong?

@janeklb
Copy link
Contributor

janeklb commented Oct 19, 2017

@heridev if you return a promise (as timdorr does), there's no need to use done

@rafaelhz
Copy link

rafaelhz commented Nov 17, 2017

what about using await/async?

test('should do something', async () => {
  await store.dispatch(actions.loadData());
  expect(store.getState().values).toEqual('test');
});

@parencik
Copy link

I got a bit fed up of writing async actions-creator tests different from sync action-creator tests, so I wrote a simple middleware that I include in the test/mock store (inserted before redux-thunk), which adds promises all the way down:

const promisifyMiddleware = ({dispatch, getState}) => next => action => {
  return new Promise( (resolve) => resolve(next(action)) )
}

This means whether an action-creator is async or not (i.e. whether it returns a promise or not), you can test it in the same way like this:

it('dispatches something', () => {
  const expectedActions = [ { type: .. } ]

  return store.dispatch(actions.maybeDoSomethingAsync())
    .then(() => {
       expect(store.getActions()).toEqual(expectedActions)
     })
})

I'd do the following instead:

import thunk from "redux-thunk";
import promiseMiddleware from 'redux-promise';

const middlewares = [promiseMiddleware, thunk];
const mockStore = configureMockStore(middlewares);

Why? @lucaspiller answer was good until I wanted to test what happens to my action creator if promise rejects instead of resolve. Now I'm able to fully cover test of my redux action creators.

Hope that helps :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests