I have an action that I want to test in my React/Redux app:
export const SET_SUBSCRIBED = 'SET_SUBSCRIBED'
export const setSubscribed = (subscribed) => {
// return {
// type: SET_SUBSCRIBED,
// subscribed: subscribed
// }
return function(dispatch) {
var url = "https://api.github.com/users/1/repos";
return fetch(url)
.then(function(result) {
if (result.status === 200) {
dispatch({
type: SET_SUBSCRIBED,
subscribed: subscribed
})
return result
}
return 'failed' //todo
})
}
}
Here is my test:
import { setSubscribed } from '../actions/subscriptions'
import nock from 'nock'
import configureMockStore from 'redux-mock-store'
import thunk from 'redux-thunk'
import * as actions from '../actions/subscriptions'
const middlewares = [ thunk ]
const mockStore = configureMockStore(middlewares)
describe('Action::Subscriptions', () => {
describe('#setSubscribed()', () => {
afterEach(() => {
nock.cleanAll()
})
describe('when subscribed is true', () => {
beforeEach(() =>{
nock('https://api.github.com/')
.get('/users/1/repos')
.reply(200, 'yes')
})
it('returns SET_SUBSCRIBED type and subscribed true', () => {
const store = mockStore({ subscribed: false })
return store.dispatch(actions.setSubscribed(true))
.then(() => {
expect(store.getActions()).toEqual([{type: 'SET_SUBSCRIBED', subscribed: true}])
})
})
})
})
})
When I run the test, I keep getting the error:
TypeError: Network request failed
It looks like my test isn't too happy with nock.
What am I doing incorrectly and how do I successfully stub the fetch() request?
Related
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 have a custom hook APIGateway calling another custom hook http. I'd like to mock the promise function sendHttpRequest to test sendAPIRequest. With this code, I am getting "Rejected to value: [TypeError: Cannot read property 'then' of undefined]"
I am trying to avoid any __mock__ files. If I mock axios, apiGateway.test passes.
How can I mock a function sendHttpRequest on the default export of useHttp?
http.js
import { useCallback } from 'react';
import axios from 'axios';
const useHttp = () => {
const sendRequest = useCallback((url, method, body) => {
return new Promise((resolve, reject) => {
axios({ method: method, url: url, data: body, config: { crossDomain: true } })
.then((response) => {
resolve(response.data);
})
.catch((error) => {
reject(error);
});
});
}, []);
return {
sendHttpRequest: sendRequest,
};
};
export default useHttp;
apiGateway.js
import { useCallback } from 'react';
import useHttp from '../abstract/http';
import configuration from '../../endpoints';
const useApiGateway = () => {
const { sendHttpRequest } = useHttp();
const apiGatewayBaseUrl = configuration.API_GATEWAY_BASE_URL;
const apiGatewayPath = configuration.LAMBDA_USER_ENDPOINT;
const sendRequest = useCallback((body) => {
return new Promise((resolve, reject) => {
sendHttpRequest(apiGatewayBaseUrl + apiGatewayPath, 'get', body)
.then((response) => {
resolve(response);
})
.catch((error) => {
reject(error);
});
});
}, []);
return {
sendApiRequest: sendRequest,
};
};
export default useApiGateway;
apiGateway.test.js
import React from 'react';
import { act, renderHook } from '#testing-library/react-hooks';
import useApiGateway from './apiGateway';
import useHttp from '../abstract/http';
jest.mock('../abstract/http', () => jest.fn());
describe('hook/aws/apiGateway', () => {
let result;
beforeEach(() => {});
it('should send GET request with no error', () => {
//TODO mock http instead of axios
let response = { data: '<html>Hello</html>' };
useHttp.mockImplementation(() => ({
sendHttpRequest: jest.fn(() => {}),
}));
let { sendHttpRequest } = useHttp();
sendHttpRequest.mockResolvedValue(
new Promise((resolve, reject) => {
resolve(response);
})
);
result = renderHook(() => useApiGateway()).result;
console.log(useHttp());
act(() => {
return expect(result.current.sendApiRequest({})).resolves.toEqual(response.data);
});
});
});
full error
Error: expect(received).resolves.toEqual()
Received promise rejected instead of resolved
Rejected to value: [TypeError: Cannot read property 'then' of undefined]
at expect (.../node_modules/expect/build/index.js:138:15)
at .../apiGateway.test.js:29:11
your mock should return a promise (rather than attempting to mock out the promise lib)
example:
function myMockRequest() {
return Promise.resolve({ mockResponse });
}
In my react application I have an async api call done with axios. And that api call does accept a custom callback.
I am able to test the axios api call using Jest + Enzyme. But not able to test the custom callback method.
Note: I have mocked my axios module.
src/mocks/axios.js
export default {
get: jest.fn(() => Promise.resolve({ data: {} })),
post: jest.fn(() => Promise.resolve({ data: {} }))
}
auth.api.js
import Axios from 'axios';
import { AUTH_SERVER_URL } from './../../settings';
import { setAuthToken } from '../actions/auth/auth.action';
export const saveUsers = (user, dispatch) => {
const URL = `${AUTH_SERVER_URL}/auth/register`;
Axios.post(URL, user)
.then(response => {
const { data } = response;
const token = {
accessToken: data.access_token,
};
return token;
})
.then(token => dispatch(setAuthToken(token)))
.catch(error => {
if (error.response) {
console.error(error.response.data.message);
}
})
}
And here is my test code.
spec.js
import mockAxios from 'axios';
import { AUTH_SERVER_URL } from './../../settings';
import { saveUsers } from './auth.api';
import { setAuthToken } from '../actions/auth/auth.action';
describe('Authentication API', () => {
it('saveUsers', () => {
const user = { x: 'test' }
const dispatch = jest.fn(); // need to test this dispatch function gets called or not
const response = {
data: {
access_token: 'access_token',
}
};
const expectedToken = {
accessToken: 'access_token',
};
mockAxios.post.mockImplementationOnce(() => Promise.resolve(response));
saveUsers(user, dispatch);
const url = `${AUTH_SERVER_URL}/auth/register`;
expect(mockAxios.post).toHaveBeenCalledTimes(1);
expect(mockAxios.post).toHaveBeenCalledWith(url, user);
console.log(dispatch.mock.calls);
expect(dispatch).toHaveBeenCalledTimes(1); // failed
expect(dispatch).toHaveBeenCalledWith(setAuthToken(expectedToken)); // failed
});
})
Please help me in this
Try to install this package flush-promises.
Then import it in your test file
import flushPromises from 'flush-promises';
And add it before your assertions.
...
await flushPromises();
expect(dispatch).toHaveBeenCalledTimes(1);
expect(dispatch).toHaveBeenCalledWith(setAuthToken(expectedToken));
And here add async.
it('saveUsers', async () => {
But I'm not sure if it will help.
Thanks to #Jakub Janik for his answer.
Bellow is my answer without using flush-promise package. But it is using the concept behind flush-promise.
import mockAxios from 'axios';
import { AUTH_SERVER_URL } from './../../settings';
import { saveUsers } from './auth.api';
import { setAuthToken } from '../actions/auth/auth.action';
// A helper function can turn that into a promise itself so you don't need to deal with the done callback.
const flushPromises = () => new Promise(resolve => setImmediate(resolve));
describe('Authentication API', () => {
it('saveUsers', async () => {
const user = { x: 'test' }
const dispatch = jest.fn(); // need to test this dispatch function gets called or not
const response = {
data: {
access_token: 'access_token',
}
};
const expectedToken = {
accessToken: 'access_token',
};
mockAxios.post.mockImplementationOnce(() => Promise.resolve(response));
saveUsers(user, dispatch);
const url = `${AUTH_SERVER_URL}/auth/register`;
expect(mockAxios.post).toHaveBeenCalledTimes(1);
expect(mockAxios.post).toHaveBeenCalledWith(url, user);
await flushPromises(); // Magic happens here
expect(dispatch).toHaveBeenCalledTimes(1);
expect(dispatch).toHaveBeenCalledWith(setAuthToken(expectedToken));
});
})
I want to test if the loginReset() function is being called every time there's an unauthorized request or response status code 401.
My code is what follows:
use-request.js
import axios from "axios"
import { axiosDefaultOptions } from "../config"
import { useSelector, useDispatch } from "react-redux"
import { loginReset } from "../store/reducers/login-slice"
const useRequest = (auth=false) => {
const request = axios.create(axiosDefaultOptions)
const dispatch = useDispatch()
if(auth){
const token = useSelector( state => state.login.data ? state.login.data.accessToken : null )
request.interceptors.request.use(config => {
config.headers.Authorization = token ? `Bearer ${token}` : ''
return config
})
request.interceptors.response.use(response => {
return response
}, error => {
if(error.response.status === 401) {
dispatch(loginReset())
}
return Promise.reject(error)
})
}
return request
}
export default useRequest
use-request.test.js
import { testHookwithStore } from "../utils"
import faker from "faker"
import { useRequest } from "../../components/hooks"
import configureStore from "redux-mock-store"
import MockAdapter from "axios-mock-adapter"
import { axiosDefaultOptions } from "../../components/config"
import thunk from "redux-thunk"
describe("useRequest", () => {
faker.seed(123);
let request = null
let authRequest = null
let token = faker.random.uuid()
const mockStore = configureStore([thunk])
let authRequestAdapter = null
const fakeDomainWord = faker.internet.domainWord()
const fakeUrl = `${axiosDefaultOptions.baseURL}/${fakeDomainWord}`
beforeEach(() => {
let store = mockStore({
login: { data: { accessToken: token } }
})
testHookwithStore(store, () => {
request = useRequest()
authRequest = useRequest(true)
authRequestAdapter = new MockAdapter(authRequest)
authRequestAdapter.onPost(fakeDomainWord, {}).reply(401, { code: 401, message: "Bad credentials" })
})
})
test("Request should have no headers", () => {
request.interceptors.request.use( config => {
expect(config.headers.Authorization).toBeNull()
})
})
test("Auth request should have Authentication Headers", () => {
authRequest.interceptors.request.use( config => {
expect(config.headers.Authorization).toBe(`Bearer ${token}`)
})
})
test("Auth request resets login when 401", async () => {
const loginReset = jest.fn()
try{
await authRequest.post(fakeUrl, {})
}
catch(error){
expect(loginReset).toHaveBeenCalledTimes(1)
}
})
})
testHookwithStore basically just creates a component wrapped around a provider. The last test is failing and I'm not sure how I would verify if the dispatch is actually working. Any clues here?
Apparently, there's a getActions() function on the mocked store.
test("Auth request resets login when 401", async () => {
try{
await authRequest.post(fakeUrl, {})
}
catch(error){
expect(store.getActions()[0].type).toBe("loginReset")
}
})