I'm creating a simple action to fetch some data from an API with Thunk. It looks like this:
import fetch from 'isomorphic-fetch';
function json(response) {
return response.json();
}
/**
* Fetches books from the server
*/
export function getBooks() {
return function(dispatch) {
fetch("http://localhost:1357/book", {mode: "cors"})
.then(json)
.then(function(data) {
dispatch({
type: "GET_BOOKS",
devices: data
});
});
}
};
It should call fetch once. I have verified that it does this, because it successfully pulls data when called in a web browser. However, when I write this test:
import fetch from 'isomorphic-fetch';
let spy = sinon.spy(fetch);
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import {getBooks} from '../../actions/getBooks';
import sinon from 'sinon';
const middlewares = [ thunk ];
const mockStore = configureMockStore(middlewares);
describe('async actions', () => {
it('calls the server', () => {
const store = mockStore({books: []});
store.dispatch(getBooks());
expect(spy.callCount).toEqual(1);
spy.restore();
});
});
However, this test fails, and the spy's call count is 0. I suspect this is due to fetch being imported by the action before the test, which is why the spy is created at the top of the file. However, this does not work. What is the recommended way to test that fetch is being called?
Reading from http://arnaudbenard.com/redux-mock-store/, part Asynchronous action.
I'm guessing it happens because you are not using a promise in your test.
it('calls the server', (done) => {
const store = mockStore({books: []});
store.dispatch(getBooks()).then(() => {
expect(spy.callCount).toEqual(1);
spy.restore();
done();
});
});
Related
I'm trying to test axios call with axios-mock-adapter. I encountered following issue:
The API calls from the test always respond to the real data instead of my mocked one with mock.onGet.
a.k. receivedActions always from the real API call, but not the expectedActions which mocked with mock.onGet.
Here is the action code (searchAction.js):
import { SEARCH_PHOTOS, FEATURED_PHOTOS } from './types';
import axiosInstence from '../apis/axiosInstence';
export const searchPhotos = (term) => (dispatch) => {
dispatch({ type: 'SEACH_REQUEST' });
return axiosInstence.get('/search/photos', {
params: {
query: term,
page: 1
}
}).then(response => {
dispatch({
type: 'SEARCH_PHOTOS',
payload: response.data
});
}).catch(error => {
dispatch({ type: 'SEACH_FAILURE' });
});
}
And my test looks like this (searchAction.test.js):
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import thunk from 'redux-thunk';
import configureMockStore from 'redux-mock-store';
import { searchPhotos } from '../searchAction';
const mock = new MockAdapter(axios);
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
const term = 'cars';
const store = mockStore({});
describe('actions', () => {
beforeEach(() => {
mock.reset();
store.clearActions();
});
it('Should create an action to signIn with a fake user', async () => {
const expectedActions = [{
type: 'SEACH_REQUEST'
}, {
type: 'SEARCH_PHOTOS',
payload: []
}];
mock.onGet('/search/photos', {
params: { term: 'cars' }
}).reply(200, expectedActions);
await store.dispatch(searchPhotos(term))
.then(data => {
const receivedActions = store.getActions();
expect(receivedActions).toEqual(expectedActions);
});
});
});
Anybody have experienced similar issue or could give me some advise.
Thanks in advence.
There are a couple of problems in your code:
First, in the action creator you are using an axios instance to make the ajax call, but in the test you are not providing that instance to the axios-mock-adapter. You should provide your axios instance in your test when you create the instance of MockAdapter.
Second, the params property you are providing to the axios mock in the onGet method does not match the parameters that are sent in the get operation in your action creator. You should match the parameters in the call with their values. Thus, you should provide query and page params.
Last, you are returning the expectedActions in the mock request, but that does not seem right. Looking at your code, it seems that you want to return an empty array.
Having all that into account, your code would look like:
import MockAdapter from 'axios-mock-adapter';
import thunk from 'redux-thunk';
import configureMockStore from 'redux-mock-store';
import axiosInstence from '../../apis/axiosInstence';
import { searchPhotos } from '../searchAction';
const mock = new MockAdapter(axiosInstence);
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
const term = 'cars';
const store = mockStore({});
describe('actions', () => {
beforeEach(() => {
mock.reset();
store.clearActions();
});
it('Should create an action to signIn with a fake user', async () => {
const expectedActions = [{
type: 'SEACH_REQUEST'
}, {
type: 'SEARCH_PHOTOS',
payload: []
}];
mock.onGet('/search/photos', {
params: {
query: 'cars',
page: 1
}
}).reply(200, []);
const data = await store.dispatch(searchPhotos(term));
const receivedActions = store.getActions();
expect(receivedActions).toEqual(expectedActions);
});
});
I'm trying to make this test, test the getImages action function. I'm getting this error
● should getImages from action function › should getImages from
action function
expect(received).toEqual(expected) // deep equality
Expected: {"data": {}, "type": "GET_IMAGES"}
Received: [Function anonymous]
ideally I want to pass in the response data, in the data object. How would I successfully do this to make the test pass?
imageActions
import { GET_IMAGES, POST_COMMENT, DELETE_IMAGE, UPLOAD_IMAGE } from './types';
import Axios from '../Axios';
export const getImages = () => {
return dispatch => {
return Axios.get('/images/uploads').then(response => {
const data = response.data;
dispatch({
type: GET_IMAGES,
data
});
});
};
};
imageActions.test.js
import React from 'react';
import { shallow, mount } from 'enzyme';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { getImages } from './imageActions';
import { GET_IMAGES } from './types';
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
describe('should getImages from action function ', () => {
it('should getImages from action function', () => {
const expected = {
type: GET_IMAGES,
data: {}
};
const actual = getImages();
expect(actual).toEqual(expected);
});
});
Since you are using redux-mock-store, you should take a look at the examples of how to test async actions.
You will also have to mock the ajax request you are doing through your Axios module. As an example, I will use jest mock to mock the Axios module.
The code would look like:
import React from 'react';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { getImages } from './imageActions';
import { GET_IMAGES } from './types';
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
// Mocking the Axios get method so that it returns something in data
jest.mock('../Axios', () => ({
get: jest.fn(() => Promise.resolve({ data: ['image1', 'image2'] }))
}));
describe('should getImages from action function ', () => {
it('should getImages from action function', () => {
const expected = {
type: GET_IMAGES,
data: ['image1', 'image2']
};
// Initialize mockstore with empty state
const store = mockStore({});
// Return the promise returned by store.dispatch so that the test waits until it's resolved.
return store.dispatch(getImages()).then(() => {
// Get the actions that have been dispatched
const actions = store.getActions();
// Check that the only action has the expected values.
expect(actions[0]).toEqual(expected);
});
});
});
Following is my action creator with multiple dispatches
export function getAnalysisById(Id){
let url = APIEndpoints["getAnalysisById"];
var dataObject = {
id: Id
}
return dispatch => {
dispatch ({
type:LOADING_ANALYSIS,
payload : { isLoading : true}
});
const request = axios.post(url, dataObject);
dispatch ({
type:GET_ANALYSIS_BY_ID,
payload: request
});
};
}
Now following is the testing code written for the above action
var chai = require('chai');
var expect = chai.expect;
const actions = require('../../src/actions/index');
import { applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import nock from 'nock'
import ReduxPromise from 'redux-promise'
const middlewares = [ thunk,ReduxPromise ]
import configureStore from 'redux-mock-store'
const mockStore = configureStore(middlewares)
describe("All actions WITH Asynchronous call",function description(){
it('should return an action to get All Analysis by id', () => {
const store = mockStore({})
// Return the promise
return store.dispatch(actions.getAnalysisById('costnomics_Actual vs Budjected'))
.then(() => {
// const actionss = store.getActions()
console.log('store',store)
})
})
});
Running the above test case produces the following error,
TypeError: Cannot read property 'then' of undefined
When I pass plain objects as return value for the action creators instead of dispatcher, everything is working fine. But when mulitple dispatchers are returned from the action creator, it gives me the above error?. Why is this happening?
Your thunk function isn't actually returning a promise. You need to have something like return request at the end so that the promise is returned from the thunk, and from there returned from store.dispatch().
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?
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.
The request is made
The response is received and the fetch is successful
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()
})
})
I follow the basic exmaple of redux.org to test async action
action.js
my code is like this:
import axios from 'axios'
export function getGoodDataStart(){
return{
type: "GOOD_DATA_START"
}
}
export function getGoodDataSuccess(payload){
console.log('success', payload)
return {
type: "GOOD_DATA_SUCCESS",
payload: payload
}
}
export function getGoodDataFail(){
return{
type: "GOOD_DATA_FAIL"
}
}
export function getGoodData(){
return (dispatch) => {
dispatch( getGoodDataStart() )
return axios.get('http://www.google.com/list')
.then( response => {
console.log('fake res',response)
dispatch(getGoodDataSuccess (response) )
})
.catch( err => {
console.log('fake err',err)
})
}
}
test.js
import nock from 'nock'
import React from 'react'
import {expect} from 'chai'
import {getGoodData} from 'registerAction'
import configureMockStore from 'redux-mock-store'
import thunk from 'redux-thunk'
const middlewares = [ thunk ]
const mockStore = configureMockStore(middlewares)
describe('Register component', () => {
it('async action', function () {
nock('http://www.google.com')
.get('/list')
.reply(200,'ok!' )
const store = mockStore({
myData: '' ,
})
const expected = [
{type: "GOOD_DATA_START"},
{type: "GOOD_DATA_SUCCESS", payload: 'ok!'}
]
return store.dispatch(getGoodData())
.then( () => {
expect(store.getActions()).to.equal(expected)
})
})
})
The problem I have is, nock is not blocking the request, it lets function getGoodData make real request to google.com. What am I doing wrong?
screen shot of the error:
Here is the demo: https://github.com/craigcosmo/react-redux-test
install: npm i
to test: npm run test
open url: http://localhost:5051/webpack-dev-server/
Typically when testing an action like this you'll want to remove anything that is not part of your action from the equation. In this case by simply using nock, you're not removing axios from the equation and are actually adding unnecessary complexity. By mocking axios with a spy, you avoid making the network call and you also avoid calling axios at all. This allows you to simply assert that axios is called with the correct parameters. The spy can return a promise that allows testing all the promise handling and subsequent action calls. In order to demonstrate this, I needed to add a library that provides spies, so I opted to add 'expect' for both assertions and spies, but you could easily do the same thing with sinon if you want to stick with chai.
Here's an example where you don't need nock at all and you just mock axios with a spy:
import React from 'react'
import * as registerAction from 'registerAction'
import configureMockStore from 'redux-mock-store'
import thunk from 'redux-thunk'
import expect from 'expect'
const middlewares = [ thunk ]
const mockStore = configureMockStore(middlewares)
// set up to mock axios methods
import axios from 'axios'
const _get = axios.get
const fakePayload = { foo: 'bar' };
describe('Register component', () => {
beforeEach(() => {
// replace the .get method temporarily with a spy
axios.get = expect.createSpy().andReturn(Promise.resolve(fakePayload));
})
afterEach(() => {
// restore the get method with our saved const
axios.get = _get;
})
it('async action', function () {
const store = mockStore({
myData: '' ,
})
const expected = [
{type: "GOOD_DATA_START"},
{type: "GOOD_DATA_SUCCESS", payload: fakePayload}
]
return store.dispatch(registerAction.getGoodData())
.then( () => {
expect(store.getActions()).toEqual(expected)
expect(axios.get).toHaveBeenCalled()
expect(axios.get).toHaveBeenCalledWith('http://www.google.com/list')
})
})
})
read https://github.com/node-nock/nock/issues/150
Your tests are doing great on console-
add this two script run on your package.json
"itest": "mocha --compilers js:babel-register -R spec \"test/*.test.js\"",
"itest:watch": "npm run itest -- --watch"
You might need something like this
beforeEach(() => {
nock.disableNetConnect();
});
afterEach(() => {
nock.cleanAll();
nock.enableNetConnect();
});
Enable/Disable real HTTP Request