I am writing a test for async action creator but encountered a problem where it states "Expected mock function to have been called" with:
[{"type": "GET_LOGIN_SUCCESS", "value": true}]
But it was not called.
I am not sure where exactly the problem is. If anyone could help that will be greatly appreciated.
Here's my actions.js
import { GET_LOGIN_SUCCESS } from './constants'
export const getLoginInfo = () => {
return (dispatch, getState, axiosInstance) => {
return axiosInstance.get('/api/isLogin.json')
.then((res) => {
dispatch({
type: GET_LOGIN_SUCCESS,
value: res.data.data.login
})
console.log('finishing dispatch')
})
}
}
actions.test.js
import { getLoginInfo } from './actions'
import { GET_LOGIN_SUCCESS } from './constants'
describe('async actions', () => {
it('dispatches GET_LOGIN_SUCCESS when getting login finishes', () => {
const axiosInstance = {
get: jest.fn(() => Promise.resolve({ data: { data: {login : true }}}))
}
const dispatch = jest.fn()
getLoginInfo()(dispatch, null, axiosInstance)
expect(dispatch).toHaveBeenCalledWith({
type: GET_LOGIN_SUCCESS,
value: true
})
})
})
The problem is that jest can't know that there are async task involved. So in your case you create a mock that returns a promise, and dispatch is called when the promise is resolved. As JavaScript is single threaded, it first evaluate the code in the test and all async tasks are done afterwards. So you need to make jest aware of the promise by using async/await:
describe('async actions', () => {
it('dispatches GET_LOGIN_SUCCESS when getting login finishes', async() => {
const p = Promise.resolve({ data: { data: {login : true }}}))
const axiosInstance = {
get: jest.fn(() => p
}
const dispatch = jest.fn()
getLoginInfo()(dispatch, null, axiosInstance)
await p // even it sounds not very logically you need to wait here
expect(dispatch).toHaveBeenCalledWith({
type: GET_LOGIN_SUCCESS,
value: true
})
})
As #brian-lives-outdoors points out, getLoginInfo returns the promise as well so you could also just wait for the result of the call:
it('dispatches GET_LOGIN_SUCCESS when getting login finishes', async() => {
const axiosInstance = {
get: jest.fn(() => Promise.resolve({ data: { data: {login : true }}}))
}
const dispatch = jest.fn()
await getLoginInfo()(dispatch, null, axiosInstance)
expect(dispatch).toHaveBeenCalledWith({
type: GET_LOGIN_SUCCESS,
value: true
})
})
There is a epic article that describes the whole topic
Related
I have written a custom hook and inside it's useEffect function I am calling an API and set the result into state. Here is my custom hook:
export const useGetUsers = (searchParams?: any | undefined, userId?: string) => {
const [users, setUsers] = useState<{
data: readonly any[] | null;
loading: boolean;
}>({
data: [],
loading: true,
});
const parsedSearchParams = {
limit: 100,
...(searchParams || {}),
};
const searchParamStr = `?${makeQueryStringFromObject(parsedSearchParams)}`;
useEffect(() => {
userRequest('users', 'get', null, searchParamStr)
.then(result => {
setUsers({
data: result?.data,
loading: false,
});
})
.catch(() => {
setUsers({
data: null,
loading: false,
});
});
}, [userId, searchParamStr]);
return { users, setUsers };
};
I want my test to get through .then(). but for some reason it does not. here is my test:
test('when the call is a success', async () => {
const spy = jest.spyOn(ES, 'userRequest');
const returnPromise = Promise.resolve({data: ['a']})
ES.userRequest = jest.fn(() => returnPromise);
const { result, waitFor} = renderHook(() => useGetUsers());
await act(() => returnPromise)
await waitFor(() => expect(spy).toHaveBeenCalled())//this fails
});
here is another try and change I made in my test, but no luck:
test('when the call is a success', async () => {
jest.mock('src/..', () => ({
...jest.requireActual('src/..'),
userRequest: jest
.fn()
.mockImplementation(() => new Promise(resolve => resolve({data: ['a']}))),
}));
const { result, waitFor} = renderHook(() => useGetUsers());
await waitFor(() => expect(ES.userRequest).toHaveBeenCalled())
});
P.S. when I mock userRequest, I expect to have the return value as I mocked. but it fails. it goes to .catch instead
I tried to use waitForNextUpdate, but no luck. I would appreciate your help
This works for me:
import { renderHook, waitFor } from '#testing-library/react';
import { useGetUsers } from '../useGetUsers';
import * as utils from '../useGetUsersUtils';
it('should work', async () => {
const mockUserRequest = jest.spyOn(utils, 'userRequest');
renderHook(() => useGetUsers());
await waitFor(() => expect(mockUserRequest).toHaveBeenCalled())
});
I am not sure where is the userRequest placed in your code. As you can see from my import it is in different file then the hook.
I have a CrudActions.js class:
export default class CrudActions {
constructor(entity, api) {
this.setEntity(entity);
this.setApi(api);
}
setEntity(entity) {
this.entity = entity.toUpperCase();
}
setApi(api) {
this.api = api;
};
getEntity() {
return this.entity;
};
getApi() {
return this.api;
};
fetchItems() {
return dispatch => {
dispatch(
{
type: `TRY_FETCH_${this.getEntity()}_ITEMS`,
}
);
this.getApi()
.fetchItems()
.then(data => {
dispatch({
type: `FETCH_${this.getEntity()}_ITEMS_SUCCEEDED`,
data
});
})
.catch(error => {
dispatch({
type: `FETCH_${this.getEntity()}_ITEMS_FAILED`,
error,
});
})
}
};
}
I extend it with a new class (one class for every route)
import { instance as api } from "../../api/app/Ping";
import CrudActions from "../base/CrudActions";
export default class PingActions extends CrudActions {
constructor() {
super("ping", api);
}
}
export const actions = new PingActions();
I want put under test fetchItems and test that right actions are dispatched.
So, in a Ping.test.js:
import { actions as pingActions } from "../../../../utils/actions/app/PingActions";
import { axiosInstance } from "../../../../utils/api/base/axiosInstance";
import MockAdapter from "axios-mock-adapter";
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
const entity = 'ping';
const baseUrl = '/ping';
const dataFetchItems = [
{
app_version: "9.8.7"
}
];
describe('Test PingActions', () => {
let mock;
let store;
beforeEach(() => {
store = mockStore({
ping: {
items: dataFetchItems
}
})
})
beforeAll(() => {
mock = new MockAdapter(axiosInstance);
});
afterEach(() => {
mock.reset();
});
it ('Test can dispatch success actions', () => {
mock.onGet('http://localhost:8000/api/v1'+baseUrl).reply(200, dataFetchItems);
store.dispatch(pingActions.fetchItems());
console.log(store.getActions());
expect(store.getActions()).toContainEqual({
type: "TRY_FETCH_PING_ITEMS",
});
});
it ('Test can dispatch fail actions', () => {
mock.onGet('http://localhost:8000/api/v1'+baseUrl).reply(401);
store.dispatch(pingActions.fetchItems());
console.log(store.getActions());
expect(store.getActions()).toContainEqual({
type: "TRY_FETCH_PING_ITEMS",
});
});
});
With these tests I can cover both case: "TRY_FETCH_PING_ITEMS" and "FETCH_PING_ITEMS_SUCCEEDED" (I see it from coverage).
I cannot understand how get FETCH_PING_ITEMS_SUCCEEDED or FETCH_PING_ITEMS_FAILED actions in store.getActions().
store.getActions() has only TRY_FETCH_PING_ITEMS inside:
PASS src/__tests__/utils/actions/app/PingActions.test.js
● Console
console.log
[ { type: 'TRY_FETCH_PING_ITEMS' } ]
at Object.<anonymous> (src/__tests__/utils/actions/app/PingActions.test.js:46:13)
console.log
[ { type: 'TRY_FETCH_PING_ITEMS' } ]
at Object.<anonymous> (src/__tests__/utils/actions/app/PingActions.test.js:55:13)
I made a new test, without luck:
it ('Test can dispatch success actions', async () => {
mock.onGet('http://localhost:8000/api/v1'+baseUrl).reply(200, dataFetchItems);
await store.dispatch(pingActions.fetchItems());
console.log(store.getActions());
expect(store.getActions()).toContainEqual({
type: "TRY_FETCH_PING_ITEMS",
});
});
But I get...
PASS src/__tests__/utils/actions/app/PingActions.test.js
● Console
console.log
[ { type: 'TRY_FETCH_PING_ITEMS' } ]
at Object.<anonymous> (src/__tests__/utils/actions/app/PingActions.test.js:46:13)
(I miss, every time, the FETCH_PING_ITEMS_SUCCEEDED)
Another test:
it ('Test can dispatch success actions', () => {
mock.onGet('http://localhost:8000/api/v1'+baseUrl).reply(200, dataFetchItems);
return store.dispatch(pingActions.fetchItems()).then(data => console.log(data));
});
But I get
TypeError: Cannot read property 'then' of undefined
Or also:
it ('Test can dispatch success actions', () => {
mock.onGet('http://localhost:8000/api/v1'+baseUrl).reply(200, dataFetchItems);
const data = pingActions.fetchItems().then(data => console.log(data));
});
I get
TypeError: _PingActions.actions.fetchItems(...).then is not a function
The Github Repository: https://github.com/sineverba/body-measurement-frontend
A few bit changes will make it work.
The Problem
You expect that FETCH_PING_ITEMS_SUCCEEDED or FETCH_PING_ITEMS_FAILED actions should be dispatched after the TRY_FETCH_PING_ITEMS action. since both success and failure cases are a promise, so they need to be processed in the proper way (nicely implemented in the CrudActions with then/catch block) but you need to handle these asynchronous actions also in your test case after dispatching the TRY_FETCH_PING_ITEMS.
The Solution
from React testing library documentation:
When in need to wait for any period of time you can use waitFor, to wait for your expectations to pass.
import {waitFor} from '#testing-library/react'
it('Test can dispatch success actions', async () => {
mock.onGet('http://localhost:8000/api/v1' + baseUrl).reply(200);
store.dispatch(pingActions.fetchItems());
expect(store.getActions()).toContainEqual({
type: "TRY_FETCH_PING_ITEMS"
})
await waitFor(() => {
expect(store.getActions()).toContainEqual({
type: "FETCH_PING_ITEMS_SUCCEEDED",
})
})
})
You can also put the fetch ping expectation in the waitFor callback.
await waitFor(() => {
expect(store.getActions()).toContainEqual({
type: "TRY_FETCH_PING_ITEMS"
})
expect(store.getActions()).toContainEqual({
type: "FETCH_PING_ITEMS_SUCCEEDED",
})
})
Note: Don't forget to add async keyword before the callback function in the it method.
Note: For failure case, do the as same as the success case.
Here's a generic example of testing a thunk, hope this helps.
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import getApiClient from './api-client';
import { initialState } from './reducer';
import * as Actions from './actions';
jest.mock('api-client');
jest.mock('actions');
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
describe('getStuff async action', () => {
it('should request data via API', async () => {
const store = mockStore({ stuff: initialState });
// Mock api client method so we're not sending out actual http requests
getApiClient.mockImplementationOnce(() => ({
getStuff: async () => [{ id: '1' }],
}));
// Don't remember if or why this part was necessary, but it was working regardless :D
const Actions = require('./actions');
// Describe the expected sequence of actions dispatched from the thunk
const expected = [
{ type: 'STUFF/REQUEST' },
{
type: 'STUFF/SUCCESS',
payload: { items: [{ id: '1' }] },
},
];
// Dispatch the thunk and wait for it to complete
await store.dispatch(Actions.getStuff('1'));
const dispatchedActions = store.getActions();
expect(dispatchedActions[0]).toEqual(expected[0]);
expect(dispatchedActions[1].payload).toEqual(expect.objectContaining(expected[1].payload));
});
});
I am pretty new to testing React-Redux and I would like to test my loadUser-action which uses redux-thunk and calls an end point which has an auth middle ware. Here is code I would like to test:
export const loadUser = () => (dispatch, getState) => {
dispatch({ type: USER_LOADING });
axios
.get('/auth/user', tokenConfig(getState))
.then((res) =>
dispatch({
type: USER_LOADED,
payload: res.data,
})
)
.catch((err) => {
console.log(err);
dispatch({
type: LOADING_FAILURE,
});
});
};
export const tokenConfig = (getState) => {
const token = getState().auth.token;
const config = {
headers: {
'Content-type': 'application/json',
},
};
if (token) {
config.headers['x-auth-token'] = token;
}
console.log('CONFIG', config);
return config;
};
And this is my test this far:
import { mockStore } from '../../test/utils/mockStore';
import { USER_LOADED } from '../types/authTypes';
import { loadUser } from './authActions';
describe('loadUser', () => {
fit('gets user', async () => {
const store = mockStore();
const tokenConfig = jest.fn();
await store.dispatch(loadUser());
const actions = store.getActions();
expect(actions[0]).toEqual({ type: USER_LOADED, meta: {} });
});
});
The tokenConfig function must be called in a different way. I can't figure out how!
I would mock axios because you don't want to be doing actual API calls when running unit tests because it would use resources on your server. Also by mocking axios, you don't have to mock tokenConfig.
This is how I have done it on a personal project of mine:
import { mockStore } from '../../test/utils/mockStore';
import { USER_LOADED, LOADING_FAILURE } from '../types/authTypes';
import { loadUser } from './authActions';
import axios from 'axios';
jest.mock('axios'); // mock axios library
describe('loadUser', () => {
fit('gets user', async () => {
const store = mockStore();
axios.get.mockImplementationOnce(() => Promise.resolve({ data: {} })); // mock resolve success
await store.dispatch(loadUser());
const actions = store.getActions();
expect(actions[0]).toEqual({ type: USER_LOADED, payload: {} });
});
it('handles api failure', () => {
const store = mockStore();
axios.get.mockImplementationOnce(() => Promise.reject('Error')); // mock error
await store.dispatch(loadUser());
const actions = store.getActions();
expect(actions[0]).toEqual({ type: LOADING_FAILURE });
});
});
I'm using react redux to create an action creator in my app. The point is that when I use async await syntax, it auto returns a promise (without the "return" keyword). However, when I use old-style promise like then(), i have to explicitly type the "return" keyword - otherwise it will return undefined. Why does this happen?
app.js (createStore):
app.get('*', (req, res) => {
const store = createStore(reducers, applyMiddleware(reduxThunk));
const promise = matchRoutes(RouteApp, req.path).map(({ route }) => {
return route.loadData ? route.loadData(store) : null;
});
console.log(promise);
Promise.all(promise).then(() => {
res.send(renderApp(req, store));
});
});
route.js:
export default [
{
loadData,
path: '/',
component: Landing,
exact: true,
},
];
landing.js
function loadData(store) {
return store.dispatch(fetchUser());
}
export { loadData };
When I use async await:
action.js
export const fetchUser = () => async (dispatch) => {
const res = await axios.get('https://react-ssr-api.herokuapp.com/users');
dispatch({
type: INFO_USER,
payload: res.data,
});
};
When I use promise then:
// It doesn't work
export const fetchUser = () => (dispatch) => {
axios.get('https://react-ssr-api.herokuapp.com/users').then((res) => {
dispatch({
type: INFO_USER,
payload: res.data,
});
});
};
"return" keyword
// now it works
export const fetchUser = () => (dispatch) => {
return axios.get('https://react-ssr-api.herokuapp.com/users').then((res) => {
dispatch({
type: INFO_USER,
payload: res.data,
});
});
};
async function always returns a promise, that's its purpose. In case there's no return value, it returns a promise of undefined.
As the reference states,
Return value
A Promise which will be resolved with the value returned by the async
function, or rejected with an uncaught exception thrown from within
the async function.
This async function
export const fetchUser = () => async (dispatch) => {
const res = await axios.get('https://react-ssr-api.herokuapp.com/users');
dispatch({
type: INFO_USER,
payload: res.data,
});
};
is syntactic sugar for this function:
export const fetchUser = () => (dispatch) => {
return axios.get('https://react-ssr-api.herokuapp.com/users').then((res) => {
dispatch({
type: INFO_USER,
payload: res.data,
});
});
};
I was testing redux actions using Jest , When i try to test the default action, it throws an Error
Expected value to equal:
{"payload": {"male": "mancha"}, "type": "actions/change_gender"}
Received:
[Function anonymous]
It seems it sends the function, instead of values.
test change_gender.js
import changeGender, { CHANGE_GENDER } from '../change_gender';
const payload = {
type: CHANGE_GENDER,
payload: {
male: 'mancha'
}
};
describe('actions', () => {
it('should Change the ', () => {
const expectedAction = {
type: payload.type,
payload: payload.payload
};
expect(changeGender('male', 'mancha')).toEqual(expectedAction)
});
});
Action change_gender.js
import toggleToolTip from './toggle_tooltip'; // eslint-disable-line
export const CHANGE_GENDER = 'actions/change_gender';
export default(radioType, type) => (dispatch) => {
dispatch({
type: CHANGE_GENDER,
payload: {
[radioType]: type
}
});
};
You should return the dispatch at change_gender.js:
change_gender.js:
import toggleToolTip from './toggle_tooltip'; // eslint-disable-line
export const CHANGE_GENDER = 'actions/change_gender';
export default(radioType, type) => (dispatch) => {
return dispatch({
type: CHANGE_GENDER,
payload: {
[radioType]: type
}
});
};
As Chen-tai mentioned, returning from the dispatch would help here for testing purposes.
The reason you see [Function] being returned is that your action is a function returning a function.
(radioType, type) => (dispatch) => { ... }
The first set of params, followed by the fat arrow is an anonymous function. That then returns another anonymous function that takes a dispatch function as its arguments. So, if we call the action twice, providing a mock dispatch function, we'll get back the expected action!
const action = (radioType, type) => (dispatch) => {
return dispatch({
type: "CHANGE_GENDER",
payload: {
[radioType]: type
}
});
};
console.log(
action('male', 'mancha')((action) => action)
)
We can then write out test:
Action change_gender.js
import toggleToolTip from './toggle_tooltip'; // eslint-disable-line
export const CHANGE_GENDER = 'actions/change_gender';
export default(radioType, type) => (dispatch) => {
return dispatch({
type: CHANGE_GENDER,
payload: {
[radioType]: type
}
});
};
test change_gender.js:
import changeGender, { CHANGE_GENDER } from '../change_gender';
const payload = {
type: CHANGE_GENDER,
payload: {
male: 'mancha'
}
};
describe('actions', () => {
it('should Change the ', () => {
const expectedAction = {
type: payload.type,
payload: payload.payload
};
expect(changeGender('male', 'mancha')((payload) => payload).toEqual(expectedAction)
});
});