Testing Action Creator with Redux Thunk & Axios - reactjs

I've got a redux-thunk action creator that makes an API request via axios, the outcome of that request then determines what sort of action is dispatched to my reducer (AUTH or UNAUTH).
This works quite well but I am unsure of what the proper way is to test this functionality. I've arrived at the solution below but have the following error in my test:
1) AUTH ACTION
returns a token on success:
TypeError: Cannot read property 'then' of undefined
Now this error leads me to believe that what i'm really getting back from my action creator isn't a promise but i'm really struggling to find a way forward.
src/actions/index.js
import axios from "axios";
import { AUTH_USER } from "./types";
const ROOT_URL = "http://localhost:";
const PORT = "3030";
export function signinUser({ email, password }) {
return ((dispatch) => {
axios
.post(`${ROOT_URL}${PORT}/signin`, { email, password })
.then(response => {
// update state to be auth'd
dispatch({ type: AUTH_USER });
// Save token locally
localStorage.setItem('token', response.data.token)
})
.catch(error => {
dispatch({ type: AUTH_ERROR, payload: error });
});
});
}
test/actions/index_test.js
import { expect } from "../test_helper";
import configureMockStore from 'redux-mock-store'
import thunk from 'redux-thunk'
import moxios from 'moxios';
import { AUTH_USER } from "../../src/actions/types";
import { signinUser } from "../../src/actions/index";
const middleware = [thunk];
const mockStore = configureMockStore(middleware);
let store;
let url;
describe('AUTH ACTION', () => {
beforeEach(() => {
moxios.install();
store = mockStore({});
url = "http://localhost:3030";
});
afterEach(() => {
moxios.uninstall();
});
it('returns a token on success', (done) => {
moxios.stubRequest(url, {
status: 200,
response: {
data: {
token: 'sample_token'
}
},
});
const expectedAction = { type: AUTH_USER }
let testData = { email: "test1#test.com", password: "1234"}
store.dispatch(signinUser(testData)).then(() => {
const actualAction = store.getActions()
expect(actualAction).to.eql(expectedAction)
})
})
})
Any help or insights would be greatly appreciated.

store.dispatch(someThunk()).then() only works if the thunk returns a promise, and your thunk isn't actually returning a promise.
If you just put a return in front of axios(), it should work.

Related

Async redux actions tests always return pending

I am writing tests for some async actions however the tests are failing because the type which is returned is always REQUEST_PENDING. So even for the tests when the data is fetched the type does not change and the test fails. I am not sure what I am doing wrong.
So the REQUEST_SUCCESS and REQUEST_FAILED are the tests that are always returning REQUEST_PENDING
This is my actions.js
import axios from 'axios';
import {
REQUEST_PENDING,
REQUEST_SUCCESS,
REQUEST_FAILED,
} from './constants';
export const setSearchField = (payload) => ({ type: SEARCH_EVENT, payload });
export const requestRobots = () => {
return async (dispatch) => {
dispatch({
type: REQUEST_PENDING,
});
try {
const result = await axios.get('//jsonplaceholder.typicode.com/users');
dispatch({ type: REQUEST_SUCCESS, payload: result.data });
} catch (error) {
dispatch({ type: REQUEST_FAILED, payload: error });
}
};
};
and this is my actions.test.js
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import {
REQUEST_PENDING,
REQUEST_SUCCESS,
REQUEST_FAILED,
} from './constants';
import * as actions from './actions';
const mock = new MockAdapter(axios);
const mockStore = configureMockStore([thunk]);
const payload = [
{
id: 1,
name: 'robocop',
email: 'robocop#gmail.com',
key: 1,
},
];
describe('handles requestRobots', () => {
beforeEach(() => {
// Runs before each test in the suite
store.clearActions();
});
const store = mockStore();
store.dispatch(actions.requestRobots());
const action = store.getActions();
it('Should return REQUEST_PENDING action', () => {
expect(action[0]).toEqual({
type: REQUEST_PENDING,
});
});
it('Should return REQUEST_SUCCESS action', () => {
mock.onGet('//jsonplaceholder.typicode.com/users').reply(200, {
data: payload,
});
return store.dispatch(actions.requestRobots()).then(() => {
const expectedActions = [
{
type: REQUEST_SUCCESS,
payload: {
data: payload,
},
},
];
expect(store.getActions()).toEqual(expectedActions);
});
});
it('Should return REQUEST_FAILURE action', () => {
mock.onGet('//jsonplaceholder.typicod.com/users').reply(400, {
data: payload,
});
return store.dispatch(actions.requestRobots()).then(() => {
const expectedActions = [
{
type: REQUEST_FAILED,
payload: {
data: ['Error: Request failed with status code 404'],
},
},
];
expect(store.getActions()).toEqual(expectedActions);
});
});
});
The lifecycle of a thunk action is that it will dispatch the REQUEST_PENDING action at the start of every call and then dispatch a REQUEST_FAILED or REQUEST_SUCCESS action at the end.
In your second and third test cases, the store.getActions() array actually has two elements: the pending action and the results action. You need to expect that the actions is an array with both. The REQUEST_FAILED and REQUEST_SUCCESS actions are there, but you aren't seeing them because they are the second element.
Define your pendingAction as a variable since you'll need it in all three tests.
const pendingAction = {
type: REQUEST_PENDING
}
Then include it in your expectedActions array.
const expectedActions = [
pendingAction,
{
type: REQUEST_SUCCESS,
payload: {
data: payload
}
}
]
This will cause your success test to pass. I did a quick run of the tests and the failure test still fails because it is not properly mocking the API failure. Right now it is returning a success because the requestRobots function uses the real axios object and not the mock axios adapter. But maybe something in your environment handles this differently.

React Redux breaking on page routing

Hello so I have added redux to the project(Ecom site) that I am working on, so I am fetching products with the help of Redux, I just then added another redux Action for when the user clicks on the product to view the details so it works and I do get back the results that I expect but now the problem is when I page route to a page which has redux I get an error until I reload the page and everything works perfectly
Code to my store
import { combineReducers, applyMiddleware, compose, createStore } from "redux";
import thunk from "redux-thunk";
import { HoodiesFecthReducer } from "./Reducers/ProductsFecthReducer/HoodiesFetchReducer";
import { JacketsFetchReducer } from "./Reducers/ProductsFecthReducer/JacketsFecthReducer";
import productDetailsFetchReducer from "./Reducers/ProductsFecthReducer /ProductDetailsFetchReducer";
import { T_ShirtsFetchReducer } from "./Reducers/ProductsFecthReducer/T-ShirtFetchReducer";
const initialState = {};
const reducers = combineReducers({
Hoodies: HoodiesFecthReducer,
Jackets: JacketsFetchReducer,
T_Shirts: T_ShirtsFetchReducer,
ProductDetail: productDetailsFetchReducer,
});
const composeEnhancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
export const store = createStore(
reducers,
initialState,
composeEnhancer(applyMiddleware(thunk))
);
Code below is one of the action that fetches the product so I will add in one because they are all the same I avoid adding tons of code that are performing same task
import {
PRODUCT_FETCH_FAIL,
PRODUCT_FETCH_REQUEST,
PRODUCT_FETCH_SUCCESS,
} from "./actionTypes";
import axios from "axios";
export const JacketsFetchAction = () => async (dispatch) => {
const url = "http://127.0.0.1:5000/api/clothes/jackets";
try {
dispatch({ type: PRODUCT_FETCH_REQUEST });
const { data } = await axios.get(url);
dispatch({ type: PRODUCT_FETCH_SUCCESS, payload: data });
} catch (error) {
dispatch({ type: PRODUCT_FETCH_FAIL, payload: error });
}
};
So now things started breaking when I added this action to the mix
import {
PRODUCT_FETCH_FAIL,
PRODUCT_FETCH_REQUEST,
PRODUCT_FETCH_SUCCESS,
} from "./actionTypes";
import axios from "axios";
const productDetailFetchAction = (productID, productCategory) => async (
dispatch
) => {
const url = `http://127.0.0.1:5000/api/clothes/product-detail/${productCategory} /${productID}`;
try {
dispatch({ type: PRODUCT_FETCH_REQUEST });
const { data } = await axios.get(url);
dispatch({ type: PRODUCT_FETCH_SUCCESS, payload: data });
} catch (error) {
dispatch({ type: PRODUCT_FETCH_FAIL, payload: error });
}
};
export default productDetailFetchAction;

axios-mock-adapter onGet mock data not effective

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);
});
});

Cannot read property '.then' of undefined when testing async action creators with redux and react

I'm trying to write some test using react, redux-mock-store and redux, but I keep getting and error. Maybe because my Promise has not yet been resolved?
The fetchListing() action creator actually works when I try it on dev and production, but I'm having problems making the test pass.
error message
(node:19143) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 3): SyntaxError
(node:19143) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
FAIL src/actions/__tests__/action.test.js
● async actions › creates "FETCH_LISTINGS" when fetching listing has been done
TypeError: Cannot read property 'then' of undefined
at Object.<anonymous> (src/actions/__tests__/action.test.js:44:51)
at Promise (<anonymous>)
at Promise.resolve.then.el (node_modules/p-map/index.js:42:16)
at <anonymous>
at process._tickCallback (internal/process/next_tick.js:169:7)
async actions
✕ creates "FETCH_LISTINGS" when fetching listing has been done (10ms)
action/index.js
// actions/index.js
import axios from 'axios';
import { FETCH_LISTINGS } from './types';
export function fetchListings() {
const request = axios.get('/5/index.cfm?event=stream:listings');
return (dispatch) => {
request.then(( { data } ) => {
dispatch({ type: FETCH_LISTINGS, payload: data });
});
}
};
action.test.js
// actions/__test__/action.test.js
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { applyMiddleware } from 'redux';
import nock from 'nock';
import expect from 'expect';
import * as actions from '../index';
import * as types from '../types';
const middlewares = [ thunk ];
const mockStore = configureMockStore(middlewares);
describe('async actions', () => {
afterEach(() => {
nock.cleanAll()
})
it('creates "FETCH_LISTINGS" when fetching listing has been done', () => {
nock('http://example.com/')
.get('/listings')
.reply(200, { body: { listings: [{ 'corpo_id': 5629, id: 1382796, name: 'masm' }] } })
const expectedActions = [
{ type: types.FETCH_LISTINGS }, { body: { listings: [{ 'corpo_id': 5629, id: 1382796, name: 'masm' }] }}
]
const store = mockStore({ listings: [] })
return store.dispatch(actions.fetchListings()).then((data) => {
expect(store.getActions()).toEqual(expectedActions)
})
})
})
store.dispatch(actions.fetchListings()) returns undefined. You can't call .then on that.
See redux-thunk code. It executes the function you return and returns that. The function you return in fetchListings returns nothing, i.e. undefined.
Try
return (dispatch) => {
return request.then( (data) => {
dispatch({ type: FETCH_LISTINGS, payload: data });
});
}
After that you will still have another problem. You don't return anything inside your then, you only dispatch. That means the next then gets undefined argument
I also know this is an old thread but you need to make sure you return the async action inside of your thunk.
In my thunk I needed to:
return fetch()
the async action and it worked
Your action creator should return a promise as shown below:
// actions/index.js
import axios from 'axios';
import { FETCH_LISTINGS } from './types';
export function fetchListings() {
return (dispatch) => {
return axios.get('/5/index.cfm?event=stream:listings')
.then(( { data } ) => {
dispatch({ type: FETCH_LISTINGS, payload: data });
});
}
};
I know this is an old thread. But I think the issue is that your action creator is not asynchronous.
Try:
export async function fetchListings() {
const request = axios.get('/5/index.cfm?event=stream:listings');
return (dispatch) => {
request.then(( { data } ) => {
dispatch({ type: FETCH_LISTINGS, payload: data });
});
}
}

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?
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()
})
})

Resources