Unable to write Redux tests for action creators - javascript

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?

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

Related

Testing a fetch.catch in custom hook

I've got this custom hook:
import React from 'react';
import { useMessageError } from 'components/Message/UseMessage';
export interface Country {
code: string;
name: string;
}
export default function useCountry(): Array<Country> {
const [countries, setCountries] = React.useState<Country[]>([]);
const { showErrorMessage } = useMessageError();
React.useEffect(() => {
fetch('/api/countries', {
method: 'GET',
})
.then(data => data.json())
.then(function(data) {
// ..
})
.catch(() => showErrorMessage());
}, []);
return countries;
}
I want to test catching an error if there will be invalid response. With that, error message should appear thanks to showErrorMessage(). And I've got this test:
const showErrorMessage = jest.fn();
jest.mock('components/Message/UseMessage', () => ({
useMessageError: () => ({
showErrorMessage: showErrorMessage,
}),
}));
import useCountry from 'components/Country/useCountry';
import { renderHook } from '#testing-library/react-hooks';
import { enableFetchMocks } from 'jest-fetch-mock';
enableFetchMocks();
describe('The useCountry hook', () => {
it('should show error message', async () => {
jest.spyOn(global, 'fetch').mockImplementation(() =>
Promise.resolve({
json: () => Promise.reject(),
} as Response),
);
const { result, waitForNextUpdate } = renderHook(() => useCountry());
await waitForNextUpdate();
expect(fetch).toHaveBeenCalled();
expect(showErrorMessage).toHaveBeenCalled();
expect(result.current).toEqual([]);
});
});
But with that, I'm getting an error:
Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.Error
What I'm doing wrong in here? I assume it is somehow related with await waitForNextUpdate();, but I really don't know for sure and how to manage with it.
waitForNextUpdate() waits for next update but your hook does not trigger it since it only calls showErrorMessage(). Take a look at this sandbox
As a straightforward solution something that triggers an update can be added:
React.useEffect(() => {
fetch('/api/countries', {
method: 'GET',
})
.then(data => data.json())
.then(function(data) {
// ..
})
.catch(() => {
showErrorMessage();
// trigger update in any suitable way, for example:
setCountries([]);
});
}, []);
But it may be better to refactor it in some way. For example, you could use a separate hook and state for errors:
export default function useCountry(): Array<Country> {
const [countries, setCountries] = React.useState<Country[]>([]);
const [error, setError] = React.useState(null);
const { showErrorMessage } = useMessageError();
React.useEffect(() => {
fetch('/api/countries', {
method: 'GET',
})
.then(data => data.json())
.then(function(data) {
// ..
})
.catch(() => setError(true));
}, []);
React.useEffect(() => {
if (error) {
showErrorMessage()
}
}, [error]);
return countries;
}

How to call callback function after dispatch action Redux

I use React Redux and I create a function to login, but I need to get a callback return after successfull login and redirect user to a page.
I try to passing function as parameter but not working.
How can I get the return after dispatch action?
Login fun
export const login = (request,cb) => {
return dispatch => {
let url = "/api/user/login";
axios({
method: "post",
url: url,
data: request,
config: { headers: { "Content-Type": "multipart/form-data" } }
})
.then(response => {
let authState = {
isLoggedIn: true,
user: response.data
};
cb();
window.localStorage["authState"] = JSON.stringify(authState);
return dispatch({
type: "USER_LOGIN_FULFILLED",
payload: { userAuthData: response.data }
});
})
.catch(err => {
return dispatch({
type: "USER_LOGIN_REJECTED",
payload: err
});
});
};
};
submiting
handleLogin(e) {
this.setState({ showLoader: true });
e.preventDefault();
const request = new Object();
if (this.validator.allValid()) {
request.email = this.state.email;
request.password = this.state.password;
this.props.login(request, () => {
//get callbach here
this.props.history.push('/my-space/my_views');
})
this.setState({ showLoader: false });
} else {
this.setState({ showLoader: false });
this.validator.showMessages();
this.forceUpdate();
}
}
const mapStateToProps = state => {
return {
authState: state
};
};
const mapDispatchToProps = dispatch => {
return {
login: request => dispatch(login(request))
};
};
export default connect(mapStateToProps, mapDispatchToProps)(LoginForm);
The cb is missing in your connect(...)
Here is the fix
handleLogin(e) {
this.setState({ showLoader: true });
e.preventDefault();
const request = new Object();
if (this.validator.allValid()) {
request.email = this.state.email;
request.password = this.state.password;
this.props.login(request, () => {
//get callbach here
this.props.history.push('/my-space/my_views');
})
this.setState({ showLoader: false });
} else {
this.setState({ showLoader: false });
this.validator.showMessages();
this.forceUpdate();
}
}
const mapStateToProps = state => {
return {
authState: state
};
};
const mapDispatchToProps = dispatch => {
return {
login: (request, cb) => dispatch(login(request, cb))
};
};
export default connect(mapStateToProps, mapDispatchToProps)(LoginForm);
Hope it helps:)
If you are using redux-thunk, you can return a Promise from your async action.
The function called by the thunk middleware can return a value,
that is passed on as the return value of the dispatch method.
In this case, we return a promise to wait for.
This is not required by thunk middleware, but it is convenient for us.
But I prefer use useEffect or componentDidUpdate for this purpose:
componentDidUpdate(){
if(this.props.authState.isLoggedIn){
this.props.history.push('/my-space/my_views');
}
}
I recommend using the Redux Cool package if you need actions with callback capability.
Instalation
npm install redux-cool
Usage
import {actionsCreator} from "redux-cool"
const my_callback = () => {
console.log("Hello, I am callback!!!")
}
const callbackable_action = actionsCreator.CALLBACKABLE.EXAMPLE(1, 2, 3, my_callback)
console.log(callbackable_action)
// {
// type: "CALLBACKABLE/EXAMPLE",
// args: [1, 2, 3],
// cb: f() my_callback,
// _index: 1
// }
callbackable_action.cb()
// "Hello, I am callback!!!"
When we try to generate an action object, we can pass the callback function as the last argument. actionsCreator will check and if the last argument is a function, it will be considered as a callback function.
See Actions Creator for more details
react-redux/redux dispatch returns a promise. you can do this if you want to return a value or identify if the request is success/error after being dispatched
Action example
export const fetchSomething = () => async (dispatch) => {
try {
const response = await fetchFromApi();
dispatch({
type: ACTION_TYPE,
payload: response.value
});
return Promise.resolve(response.value);
} catch (error) {
return Promise.reject(error);
}
}
Usage
const foo = async data => {
const response = new Promise((resolve, reject) => {
dispatch(fetchSomething())
.then(v => resolve(v))
.catch(err => reject(err))
});
await response
.then((v) => navigateToSomewhere("/", { replace: true }))
.catch(err => console.log(err));
};
this post is old, but hopefully it will help
Package.json
"react-redux": "^8.0.2"
"#reduxjs/toolkit": "^1.8.5"

How can I test chained promises in a Jest test?

Below I have a test for my login actions. I'm mocking a Firebase function and want to test if the signIn/signOut functions are called.
The tests pass. However, I do not see my second console log. Which is this line console.log('store ==>', store);.
it('signIn should call firebase', () => {
const user = {
email: 'first.last#yum.com',
password: 'abd123'
};
console.log('111');
return store.dispatch(signIn(user.email, user.password)).then(() => {
console.log('222'); // Does not reach
expect(mockFirebaseService).toHaveBeenCalled();
});
console.log('333');
});
● login actions › signIn should call Firebase
TypeError: auth.signInWithEmailAndPassword is not a function
Action being tested
// Sign in action
export const signIn = (email, password, redirectUrl = ROUTEPATH_DEFAULT_PAGE) => (dispatch) => {
dispatch({ type: USER_LOGIN_PENDING });
return firebase
.then(auth => auth.signInWithEmailAndPassword(email, password))
.catch((e) => {
console.error('actions/Login/signIn', e);
// Register a new user
if (e.code === LOGIN_USER_NOT_FOUND) {
dispatch(push(ROUTEPATH_FORBIDDEN));
dispatch(toggleNotification(true, e.message, 'error'));
} else {
dispatch(displayError(true, e.message));
setTimeout(() => {
dispatch(displayError(false, ''));
}, 5000);
throw e;
}
})
.then(res => res.getIdToken())
.then((idToken) => {
if (!idToken) {
dispatch(displayError(true, 'Sorry, there was an issue with getting your token.'));
}
dispatch(onCheckAuth(email));
dispatch(push(redirectUrl));
});
};
Full Test
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
// Login Actions
import {
// onCheckAuth,
signIn,
signOut
} from 'actions';
import {
// USER_ON_LOGGED_IN,
USER_ON_LOGGED_OUT
} from 'actionTypes';
// String Constants
// import { LOGIN_USER_NOT_FOUND } from 'copy';
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
// Mock all the exports in the module.
function mockFirebaseService() {
return new Promise(resolve => resolve(true));
}
// Since "services/firebase" is a dependency on this file that we are testing,
// we need to mock the child dependency.
jest.mock('services/firebase', () => new Promise(resolve => resolve(true)));
describe('login actions', () => {
let store;
beforeEach(() => {
store = mockStore({});
});
it('signIn should call firebase', () => {
const user = {
email: 'first.last#yum.com',
password: 'abd123'
};
console.log('111');
return store.dispatch(signIn(user.email, user.password)).then(() => {
console.log('222'); // does not reach
expect(mockFirebaseService).toHaveBeenCalled();
});
console.log('333');
});
it('signOut should call firebase', () => {
console.log('signOut should call firebasew');
store.dispatch(signOut()).then(() => {
expect(mockFirebaseService).toHaveBeenCalled();
console.log('store ==>', store);
expect(store.getActions()).toEqual({
type: USER_ON_LOGGED_OUT
});
});
console.log('END');
});
});
You have two issues here,
The tests pass however I do not see my 2nd console log. Which is this
line console.log('store ==>', store);.
That is because the test is not waiting for the promise to fulfill, so you should return it:
it('signOut should call firebase', () => {
console.log('signOut should call firebasew');
return store.dispatch(signOut()).then(() => { // NOTE we return the promise
expect(mockFirebaseService).toHaveBeenCalled();
console.log('store ==>', store);
expect(store.getActions()).toEqual({
type: USER_ON_LOGGED_OUT
});
console.log('END');
});
});
You can find examples in the Redux official documentation.
Secondly, your signIn test is failing because you have mocked the wrong firebase:
jest.mock('services/firebase', () => new Promise(resolve => resolve(true)));
That should probably look more like:
jest.mock('services/firebase', () => new Promise(resolve => resolve({
signInWithEmailAndPassword: () => { return { getIdToken: () => '123'; } }
})));
Login actions › signIn should call firebase
TypeError: auth.signInWithEmailAndPassword is not a function
This tells that your store.dispatch(signIn(user.email, user.password)) fails, thus your second console.log won't go into your then chain, use catch or second callback argument of then instead.

Trying to mock delay in API response: Cannot read property 'then' of undefined

In my React app I'm trying to fake a delay in mock API response
Uncaught TypeError: Cannot read property 'then' of undefined
createDraft() {
// Post to Mock API
const newDraft = {
name: this.state.draftName,
created_by: this.props.user,
created: getDate()
};
/* ERROR HERE */
this.props.create(newDraft).then((res) => {
console.log(' res', res);
// Display loading spinner while waiting on Promise
// Close modal and redirect to Draft Summary view
this.props.closeModal();
});
}
The action:
export const create = newDraft => dispatch => all()
.then(() => setTimeout(() => {
dispatch({
type: CREATE,
newDraft
});
return 'Draft created!';
}, 2000))
.catch(() => {
dispatch({
type: REQUEST_ERROR,
payload: apiErrors.BAD_REQUEST
});
});
Connected area:
const mapDispatchToProps = dispatch => ({
create: (newDraft) => { dispatch(create(newDraft)); }
});
export const CreateDraftModalJest = CreateDraftModal;
export default connect(cleanMapStateToProps([
'user'
]), mapDispatchToProps)(CreateDraftModal);
Also tried this with same result
function DelayPromise(t) {
return new Promise(((resolve) => {
setTimeout(resolve, t);
}));
}
export const create = newDraft => dispatch => all()
.then(DelayPromise(2000))
.then(() => {
console.log('created called', newDraft);
dispatch({
type: CREATE,
newDraft
});
})
.then(() => 'Draft created!')
.catch(() => {
dispatch({
type: REQUEST_ERROR,
payload: apiErrors.BAD_REQUEST
});
});
It's possible that your this.props.create() action creator is not bound to dispatch, and therefore not returning the promise as expected.
Are you using react-redux connect() properly?
EDIT:
Your mapDispatchToProps is not returning the promise. Do this instead:
const mapDispatchToProps = dispatch => ({
create: (newDraft) => dispatch(create(newDraft)),
});

how to write test cases for redux async actions using axios?

Following is a sample async action creator.
export const GET_ANALYSIS = 'GET_ANALYSIS';
export function getAllAnalysis(user){
let url = APIEndpoints["getAnalysis"];
const request = axios.get(url);
return {
type:GET_ANALYSIS,
payload: request
}
}
Now following is the test case I have wrote:
describe('All actions', function description() {
it('should return an action to get All Analysis', (done) => {
const id = "costnomics";
const expectedAction = {
type: actions.GET_ANALYSIS
};
expect(actions.getAllAnalysis(id).type).to.eventually.equal(expectedAction.type).done();
});
})
I am getting the following error:
All actions should return an action to get All Analysis:
TypeError: 'GET_ANALYSIS' is not a thenable.
at assertIsAboutPromise (node_modules/chai-as-promised/lib/chai-as-promised.js:29:19)
at .<anonymous> (node_modules/chai-as-promised/lib/chai-as-promised.js:47:13)
at addProperty (node_modules/chai/lib/chai/utils/addProperty.js:43:29)
at Context.<anonymous> (test/actions/index.js:50:5)
Why is this error coming and how can it be solved?
I suggest you to take a look at moxios. It is axios testing library written by axios creator.
For asynchronous testing you can use mocha async callbacks.
As you are doing async actions, you need to use some async helper for Redux. redux-thunk is most common Redux middleware for it (https://github.com/gaearon/redux-thunk). So assuming you'll change your action to use dispatch clojure:
const getAllAnalysis => (user) => dispatch => {
let url = APIEndpoints["getAnalysis"];
const request = axios.get(url)
.then(response => disptach({
type:GET_ANALYSIS,
payload: response.data
}));
}
Sample test can look like this:
describe('All actions', function description() {
beforeEach("fake server", () => moxios.install());
afterEach("fake server", () => moxios.uninstall());
it("should return an action to get All Analysis", (done) => {
// GIVEN
const disptach = sinon.spy();
const id = "costnomics";
const expectedAction = { type: actions.GET_ANALYSIS };
const expectedUrl = APIEndpoints["getAnalysis"];
moxios.stubRequest(expectedUrl, { status: 200, response: "dummyResponse" });
// WHEN
actions.getAllAnalysis(dispatch)(id);
// THEN
moxios.wait(() => {
sinon.assert.calledWith(dispatch, {
type:GET_ANALYSIS,
payload: "dummyResponse"
});
done();
});
});
});
I found out it was because, I had to use a mock store for the testing along with "thunk" and "redux-promises"
Here is the code which made it solve.
const {expect} = require('chai');
const actions = require('../../src/actions/index')
import ReduxPromise from 'redux-promise'
import thunk from 'redux-thunk'
const middlewares = [thunk,ReduxPromise]
import configureStore from 'redux-mock-store'
const mockStore = configureStore(middlewares)
describe('store middleware',function description(){
it('should execute fetch data', () => {
const store = mockStore({})
// Return the promise
return store.dispatch(actions.getAllDashboard('costnomics'))
.then(() => {
const actionss = store.getActions()
console.log('actionssssssssssssssss',JSON.stringify(actionss))
// expect(actionss[0]).toEqual(success())
})
})
})

Categories

Resources