Search code examples
javascriptreactjsreduxredux-thunknock

Unable to write Redux tests for action creators


ORIGINAL QUESTION

I'm following the example for writing tests for async action creators spelled out in the Redux documentation. I'm following the example as closely as possible, but I can't get the test to work. I'm getting the following error message:

   TypeError: Cannot read property 'then' of undefined
   (node:789) UnhandledPromiseRejectionWarning: Unhandled promise rejection 
   (rejection id: 28): TypeError: Cannot read property 'data' of undefined

Here is the code for my action creator and test:

actions/index.js

import axios from 'axios';
import { browserHistory } from 'react-router';
import { AUTH_USER, AUTH_ERROR, RESET_AUTH_ERROR } from './types';

const API_HOST = process.env.NODE_ENV == 'production'
                ? http://production-server
                : 'http://localhost:3090';

export function activateUser(token) {
  return function(dispatch) {
    axios.put(`${API_HOST}/activations/${token}`)
      .then(response => {
        dispatch({ type: AUTH_USER });

        localStorage.setItem('token', response.data.token);
      })
      .catch(error => {
        dispatch(authError(error.response.data.error));
      });
  }
}

export function authError(error) {
  return {
    type: AUTH_ERROR,
    payload: error
  }
}

confirmation_test.js

import configureMockStore from 'redux-mock-store'; 
import thunk from 'redux-thunk';
import * as actions from '../../src/actions';
import { AUTH_USER, AUTH_ERROR, RESET_AUTH_ERROR } from 
'../../src/actions/types';
import nock from 'nock';
import { expect } from 'chai';

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

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

  it('dispatches AUTH_USER', (done) => {
    nock('http://localhost:3090')
    .put('/activations/123456')
    .reply(200, {
        token: 7891011
    });

    const expectedActions = { type: AUTH_USER };

    const store = mockStore({});

    return store.dispatch(actions.activateUser(123456))
     .then(() => { // return of async actions
       expect(store.getActions()).toEqual(expectedActions);
       done();
     });
  });
});

UPDATED QUESTION

I've partially (though not entirely) figured this out. I got this to work by adding a return statement in front of axios and commenting out the localstorage.setItem call.

I also turned the object I assigned to expectedActions to an array, and changed my assertion from toEqual to to.deep.equal. Here is the modified code:

actions/index.js

export function activateUser(token) {
  return function(dispatch) { // added return statement
    return axios.put(`${API_HOST}/activations/${token}`)
      .then(response => {
        dispatch({ type: AUTH_USER });
        // localStorage.setItem('token', response.data.token); Had to comment out local storage
      })
      .catch(error => {
        dispatch(authError(error.response.data.error));
      });
  }
}

confirmation_test.js

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

  it('dispatches AUTH_USER', (done) => {
    nock('http://localhost:3090')
    .put('/activations/123456')
    .reply(200, {
        token: 7891011
     });

    const expectedActions = [{ type: AUTH_USER }];

    const store = mockStore({});

    return store.dispatch(actions.activateUser(123456))
     .then(() => { // return of async actions
       expect(store.getActions()).to.deep.equal(expectedActions);
       done();
     });
   });
 });

But now I can't test localStorage.setItem without producing this error message:

Error: timeout of 2000ms exceeded. Ensure the done() callback is being called 
in this test.

Is this because I need to mock out localStorage.setItem? Or is there an easier solve that I'm missing?


Solution

  • I figured out the solution. It involves the changes I made in my updated question as well as adding a mock of localStorage to my test_helper.js file. Since there seems to be a lot of questions about this online, I figured perhaps my solution could help someone down the line.

    test_helper.js

    import jsdom from 'jsdom';
    
    global.localStorage = storageMock();
    
    global.document = jsdom.jsdom('<!doctype html><html><body></body></html>');
    global.window = global.document.defaultView;
    global.navigator = global.window.navigator;
    
    global.window.localStorage = global.localStorage;
    
    // localStorage mock
    
    function storageMock() {
        var storage = {};
    
        return {
          setItem: function(key, value) {
            storage[key] = value || '';
          },
          getItem: function(key) {
            return key in storage ? storage[key] : null;
          },
          removeItem: function(key) {
            delete storage[key];
          }
        };
      }
    

    actions.index.js

    export function activateUser(token) {
      return function(dispatch) {
        return axios.put(`${API_HOST}/activations/${token}`)
          .then(response => {
            dispatch({ type: AUTH_USER });
            localStorage.setItem('token', response.data.token);
          })
          .catch(error => {
            dispatch(authError(error.response.data.error));
          });
      }
    }
    

    confirmation_test.js

    describe('Confirmation action creator', () => {
      afterEach(() => {
        nock.cleanAll()
      });
    
      it('dispatches AUTH_USER and stores token in localStorage', (done) => {
        nock('http://localhost:3090')
        .put('/activations/123456')
        .reply(200, {
            token: '7891011'
        });
    
        const expectedActions = [{ type: AUTH_USER }];
    
        const store = mockStore({});
    
        return store.dispatch(actions.activateUser(123456))
         .then(() => { // return of async actions
           expect(store.getActions()).to.deep.equal(expectedActions);
           expect(localStorage.getItem('token')).to.equal('7891011');
           done();
        });
      });
    });