Search code examples
reactjsjestjsfetchsinonredux-thunk

Sinon spy with isomorphic-fetch


I created a simple thunk action to get data from an API. It looks like this:

import fetch from 'isomorphic-fetch';

function json(response) {
  return response.json();
}

/**
 * Fetches booksfrom the server
 */
export function getBooks() {
  return function(dispatch) {
    return fetch("http://localhost:1357/book", {mode: "cors"})
    .then(json)
    .then(function(data) {
      dispatch({
        type: "GET_Books",
        books: data
      });
      // This lets us use promises if we want
      return(data);
    });
  }
};

Then, I wrote a test like this:

import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import {getBooks} from '../../actions/getBooks';
import nock from 'nock';
import fetch from 'isomorphic-fetch';
import sinon from 'sinon';

it('returns the found devices', () => {
  var devices = nock("http://localhost:1357")
                .get("/book")
                .reply(200,
                      {});
  const store = mockStore({devices: []});
  var spy = sinon.spy(fetch);
  return store.dispatch(getBooks()).then(() => {
  }).catch((err) => {
  }).then(() => {
    // https://gist.github.com/jish/e9bcd75e391a2b21206b
    expect(spy.callCount).toEqual(1);
    spy.retore();
  });
});

This test fails - the call count is 0, not 1. Why isn't sinon mocking the function, and what do I need to do to make it mock the function?


Solution

  • You are importing fetch in your test file and not calling it anywhere. That is why call count is zero.

    This begs the question of why you are testing that the action creator is called in the first place when the test description is "returns the found devices".

    The main purpose of thunk action creators is to be an action creator which returns a function that can be called at a later time. This function that is called at a later time can receive the stores dispatch and state as its arguments. This allows the returned function to dispatch additional actions asynchronously.

    When you are testing a thunk action creator you should be focus on whether or not the correct actions are dispatched in the following cases.

    1. The request is made

    2. The response is received and the fetch is successful

    3. An error occurs and the fetch failed

    Try something like the following:

    export function fetchBooksRequest () {
      return {
        type: 'FETCH_BOOKS_REQUEST'
      }
    }
    
    export function fetchBooksSuccess (books) {
      return {
        type: 'FETCH_BOOKS_SUCCESS',
        books: books
      }
    }
    
    export function fetchBooksFailure (err) {
      return {
        type: 'FETCH_BOOKS_FAILURE',
        err
      }
    }
    
    
    /**
     * Fetches books from the server
     */
    export function getBooks() {
      return function(dispatch) {
        dispatch(fetchBooksRequest(data));
    
        return fetch("http://localhost:1357/book", {mode: "cors"})
        .then(json)
        .then(function(data) {
          dispatch(fetchBooksSuccess(data));
          // This lets us use promises if we want
          return(data);
        }).catch(function(err) {
          dispatch(fetchBooksFailure(err));
        })
      }
    };
    

    Tests.js

    import configureMockStore from 'redux-mock-store'
    import thunk from 'redux-thunk'
    import fetchMock from 'fetch-mock'  // You can use any http mocking library
    
    import {getBooks} from '../../actions/getBooks';
    
    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 = [ 
            'FETCH_BOOKS_REQUEST', 
            'FETCH_BOOKS_SUCCESS'
        ]
    
     // Mock the fetch() global to always return the same value for GET
     // requests to all URLs.
     fetchMock.get('*', { response: 200 })
    
        return store.dispatch(fetchBooks())
          .then(() => {
            const actualActions = store.getActions().map(action => action.type)
            expect(actualActions).toEqual(expectedActions)
         })
    
        fetchMock.restore()
      })
    
      it('expected actions should be dispatched on failed request', () => {
        const store = mockStore({})
        const expectedActions = [ 
            'FETCH_BOOKS_REQUEST', 
            'FETCH_BOOKS_FAILURE'
        ]
     // Mock the fetch() global to always return the same value for GET
     // requests to all URLs.
     fetchMock.get('*', { response: 404 })
    
        return store.dispatch(fetchBooks())
          .then(() => {
            const actualActions = store.getActions().map(action => action.type)
            expect(actualActions).toEqual(expectedActions)
         })
    
        fetchMock.restore()
      })
    })