kibowki
kibowki

Reputation: 4376

How to unit test this Redux thunk?

So I have this Redux action creator that is using redux thunk middleware:

accountDetailsActions.js:

export function updateProduct(product) {
  return (dispatch, getState) => {
    const { accountDetails } = getState();

    dispatch({
      type: types.UPDATE_PRODUCT,
      stateOfResidence: accountDetails.stateOfResidence,
      product,
    });
  };
}

How do I test it? I'm using the chai package for testing. I have found some resources online, but am unsure of how to proceed. Here is my test so far:

accountDetailsReducer.test.js:

describe('types.UPDATE_PRODUCT', () => {
    it('should update product when passed a product object', () => {
        //arrange
        const initialState = {
            product: {}
        };
        const product = {
            id: 1,
            accountTypeId: 1,
            officeRangeId: 1,
            additionalInfo: "",
            enabled: true
        };
        const action = actions.updateProduct(product);
        const store = mockStore({courses: []}, action);
        store.dispatch(action);
        //this is as far as I've gotten - how can I populate my newState variable in order to test the `product` field after running the thunk?
        //act
        const newState = accountDetailsReducer(initialState, action);
        //assert
        expect(newState.product).to.be.an('object');
        expect(newState.product).to.equal(product);
    });
});

My thunk doesn't do any asynchronous actions. Any advice?

Upvotes: 19

Views: 25062

Answers (4)

Malaji Nagaraju
Malaji Nagaraju

Reputation: 151

import {createStore, applyMiddleWare, combineReducers} from 'redux';
import contactsReducer from '../reducers/contactsReducer';

function getThunk(dispatchMock){

 return ({dispatch, getState}) => (next) => (action) => {

  if(typeof action === 'function'){
   // dispatch mock function whenever we are dispatching thunk action
   action(dispatchMock, getState);

   return action(dispatch, getState);
  }
  // dispatch mock function whenever we are dispatching action
  dispatchMock(action);
  return next(action);
 }

}

describe('Test Redux reducer with Thunk Actions', ()=>{

 test('should add contact on dispatch of addContact action',()=>{
   // to track all actions dispatched from store, creating mock.
   const storeDispatchMock = jest.fn();

   const reducer = combineReducers({
     Contacts: contactsReducer
   })

   const store = createStore( reducer, {Contacts:[]}, applyMiddleware(getThunk(storeDispatchMock));

   store.dispatch(someThunkAction({name:"test1"}))
   expect(storeDispatchMock).toHaveBeenCalledWith({
    type:'contactUpdate', payload:{{name:"test1"}}
   })
   expect(store.getState()).toBe({Contacts:[{name:"test1"}]})
 })

})

Upvotes: 0

Krystian Mężyk
Krystian Mężyk

Reputation: 1

export const someAsyncAction = (param) => (dispatch, getState) => {
    const { mock } = getState();
    dispatch({
        type: 'SOME_TYPE',
        mock: mock + param,
    })
}

it('should test someAsyncAction', () => {
    const param = ' something';
    const dispatch = jest.fn().mockImplementation();
    const getState = () => ({
        mock: 'mock value',
    });

    const expectedAction = {
        type: 'SOME_TYPE',
        mock: 'mock value something'
    };

    const callback = someAsyncAction(param);
    expect(typeof callback).toBe('function');

    callback.call(this, dispatch, getState);
    expect(dispatch.mock.calls[0]).toEqual([expectedAction])
});

Upvotes: 0

therewillbecode
therewillbecode

Reputation: 7180

How to Unit Test Redux Thunks

The whole point of a thunk action creator is to dispatch asynchronous actions in the future. When using redux-thunk a good approach is to model the async flow of beginning and end resulting in success or an error with three actions.

Although this example uses Mocha and Chai for testing you could quite as easily use any assertion library or testing framework.

Modelling the async process with multiple actions managed by our main thunk action creator

Let us assume for the sake of this example that you want to perform an asynchronous operation that updates a product and want to know three crucial things.

  • When the async operation begins
  • When the async operation finishes
  • Whether the async operation succeeded or failed

Okay so time to model our redux actions based on these stages of the operation's lifecycle. Remember the same applies to all async operations so this would commonly be applied to http requests to fetch data from an api.

We can write our actions like so.

accountDetailsActions.js:

export function updateProductStarted (product) {
  return {
    type: 'UPDATE_PRODUCT_STARTED',
    product,
    stateOfResidence
  }
}

export function updateProductSuccessful (product, stateOfResidence, timeTaken) {
  return {
    type: 'PRODUCT_UPDATE_SUCCESSFUL',
    product,
    stateOfResidence
    timeTaken
  }
}

export function updateProductFailure (product, err) {
  return {
    product,
    stateOfResidence,
    err
  }
}

// our thunk action creator which dispatches the actions above asynchronously
export function updateProduct(product) {
  return dispatch => {
    const { accountDetails } = getState()
    const stateOfResidence = accountDetails.stateOfResidence

    // dispatch action as the async process has begun
    dispatch(updateProductStarted(product, stateOfResidence))

    return updateUser()
        .then(timeTaken => {
           dispatch(updateProductSuccessful(product, stateOfResidence, timeTaken)) 
        // Yay! dispatch action because it worked
      }
    })
    .catch(error => {
       // if our updateUser function ever rejected - currently never does -
       // oh no! dispatch action because of error
       dispatch(updateProductFailure(product, error))

    })
  }
}

Note the busy looking action at the bottom. That is our thunk action creator. Since it returns a function it is a special action that is intercepted by redux-thunk middleware. That thunk action creator can dispatch the other action creators at a point in the future. Pretty smart.

Now we have written the actions to model an asynchronous process which is a user update. Let's say that this process is a function call that returns a promise as would be the most common approach today for dealing with async processes.

Define logic for the actual async operation that we are modelling with redux actions

For this example we will just create a generic function that returns a promise. Replace this with the actual function that updates users or does the async logic. Ensure that the function returns a promise.

We will use the function defined below in order to create a working self-contained example. To get a working example just throw this function in your actions file so it is in the scope of your thunk action creator.

 // This is only an example to create asynchronism and record time taken
 function updateUser(){
      return new Promise( // Returns a promise will be fulfilled after a random interval
          function(resolve, reject) {
              window.setTimeout(
                  function() {
                      // We fulfill the promise with the time taken to fulfill
                      resolve(thisPromiseCount);
                  }, Math.random() * 2000 + 1000);
          }
      )
})

Our test file

import configureMockStore from 'redux-mock-store'
import thunk from 'redux-thunk'
import chai from 'chai' // You can use any testing library
let expect = chai.expect;

import { updateProduct } from './accountDetailsActions.js'

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

describe('Test thunk action creator', () => {
  it('expected actions should be dispatched on successful request', () => {
    const store = mockStore({})
    const expectedActions = [ 
        'updateProductStarted', 
        'updateProductSuccessful'
    ]

    return store.dispatch(fetchSomething())
      .then(() => {
        const actualActions = store.getActions().map(action => action.type)
        expect(actualActions).to.eql(expectedActions)
     })

  })

  it('expected actions should be dispatched on failed request', () => {
    const store = mockStore({})
    const expectedActions = [ 
        'updateProductStarted', 
        'updateProductFailure'
    ]

    return store.dispatch(fetchSomething())
      .then(() => {
        const actualActions = store.getActions().map(action => action.type)
        expect(actualActions).to.eql(expectedActions)
     })

  })
})

Upvotes: 20

Mario Tacke
Mario Tacke

Reputation: 5498

Have a look at Recipe: Writing Tests from the official documentation. Also, what are you testing, the action creator or the reducer?

Action Creator Test Example

describe('types.UPDATE_PRODUCT', () => {
    it('should update product when passed a product object', () => {    
        const store = mockStore({courses: []});
        const expectedActions = [
            / * your expected actions */
        ];

        return store.dispatch(actions.updateProduct(product))
            .then(() => {
                expect(store.getActions()).to.eql(expectedActions);
            });
    });
});

Reducer Test Example

Your reducer should be a pure function, so you can test it in isolation outside of the store environment.

const yourReducer = require('../reducers/your-reducer');

describe('reducer test', () => {
    it('should do things', () => {
        const initialState = {
            product: {}
        };

        const action = {
            type: types.UPDATE_PRODUCT,
            stateOfResidence: // whatever values you want to test with,
            product: {
                id: 1,
                accountTypeId: 1,
                officeRangeId: 1,
                additionalInfo: "",
                enabled: true
            }
        }

        const nextState = yourReducer(initialState, action);

        expect(nextState).to.be.eql({ /* ... */ });
    });
});

Upvotes: 8

Related Questions