I want to test the api call and data returned which should be displayed inside my functional component. I have a component that calls an API when it is first loaded and when certain things change i.e when typing.
I have a useEffect calling the API like so:
useEffect(() => {
const query = queryString;
const apiRequest = async () => {
try {
setIsFetching(true);
const response = await getData(query, page);
setData(response.data);
} catch (error) {
// Do some error
console.log('error');
} finally {
setIsFetching(false);
}
};
apiRequest();
}, [queryString, page]);
getData is an axios function like so:
let getDataRequest;
const getData = (searchQuery = null, page = PAGE, size = PAGE_SIZE) => {
if (getDataRequest) getDataRequest.cancel();
getDataRequest = axios.CancelToken.source();
return axios.get('url_to_api', {
params: {
page,
searchQuery,
size,
},
cancelToken: getDataRequest.token,
});
};
When trying to test this component I am running into the error When testing, code that causes React state updates should be wrapped into act(...):
I have been trying to follow first link also second link third link and many more pages but still no luck.
Here is one version of the test I am still seeing the error:
it('should render data', async () => {
let container;
act(async () => {
await getData.mockResolvedValueOnce({
data: { things: data, meta: { current_page: 1 } },
});
container = render(<Component />);
});
await waitFor(() => container.queryByText('Text I would expect after api call'));
})
I have also tried mocking the function in my test file like so:
import { getData } from './dataAccess';
const mockedData = { data: { things: data } };
jest.mock('./api', () => ({
getData: jest
.fn()
.mockImplementation(() => new Promise(resolve => resolve(mockedData))),
}));
I am using import { act } from 'react-dom/test-utils'; with enzyme and jest. I also am using '#testing-library/react';
Related
I am pulling data from an api as async and transferring the payload value returned from this api to the state field in the store with store.dispatch.
But at first this state is empty. When I make a change on the page and render it, the state is filled.
the function I pulled the api
const getTransferredOrder = async () => {
isLoading.value = true;
return await TransferredOrderService.getTransferredOrderSummary()
.then((payload) => {
store.dispatch('GetTransferredList',payload)
return payload;
})
.catch(() => {
return [];
}).finally(() => {
isLoading.value = false
});
}
i call this function first in onmounted
onMounted(async () => {
await getTransferredOrder()
})
my actions,mutations and state js files
actions.js
const actions={
GetTransferredList({commit},payload){
commit('GET_TRANSFERRED_ORDER_LIST',payload)
},
}
export default actions
mutations.js
const mutations={
GET_TRANSFERRED_ORDER_LIST(state,payload){
state.transferredOrderList = payload;
},
}
export default mutations
state.js here
const state={
transferredOrderList:[],
}
export default state
Since the state is empty in the place where I store.dispatch, it is also empty on other pages.
But when I write a small console.log on the page and compile it, the store.state fills the page. What exactly does that have to do with it?
Can you look at console and write errors here.
Why are you returns? Can you change code to ->
const getTransferredOrder = async () => {
isLoading.value = true;
await TransferredOrderService.getTransferredOrderSummary()
.then((payload) => {
store.dispatch('GetTransferredList',payload);
})
.catch(() => {
store.dispatch('GetTransferredList',[]);
}).finally(() => {
isLoading.value = false
});
}
im using an http request function as the handler function in middy and then use the ssm middleware to fetch some ssm parameters before initiating the http request.
like this:
const makeThirdPartyServiceRequest = middy(async ({ params }) => {
logger.info(`SENDING Request to ${endpoint} API`)
const url = `https://someurltoathirdpartyservice`
const options = {
method: 'POST',
body: params
}
return helpers.makeRequest(url, options)
})
makeThirdPartyServiceRequest.use(ssm(......))
However in my jest unit test Im trying to mock makeThirdPartyServiceRequest and explicitly say it should resolve to a value:
jest.mock('../src/thirdPartyService', () => ({
__esModule: true,
default: {
...(jest.requireActual('../src/thirdPartyService') as { default: {} }).default,
makeThirdPartyServiceRequest: jest.fn()
}
}))
export {}
import thirdPartyService from '../src/thirdPartyService'
And then in the test i say:
describe('makeThirdPartyServiceRequest()', () => {
it('should makeThirdPartyServiceRequest', async () => {
// Given
// })
const mockedThirdPartyServiceRequest = mocked(thirdPartyService.makeThirdPartyServiceRequest).mockResolvedValue({})
// When
const result = await thirdPartyService.makeThirdPartyServiceRequest(something)
// Then
expect(mockedThirdPartyServiceRequest).toHaveBeenCalledTimes(1)
expect(mockedThirdPartyServiceRequest.mock.calls[0][0].params.toString()).toBe(expectedParams)
})
})
However for some reason the middy middleware is still being invoked, which i clearly dont want and i have tried to mock away... what am i doing wrong?
You need to mock middy instead, to make it becomes a useless function. That function recipe a function as a parameter and return that parameter.
import thirdPartyService from '../src/thirdPartyService'
jest.mock('#middy/core', () => {
return (handler) => {
return {
use: jest.fn().mockReturnValue(handler), // ...use(ssm()) will return handler function
}
}
})
describe('thirdPartyService()', () => {
beforeEach(() => {
jest.spyOn(helpers, 'makeRequest') // spy on helpers unit
})
describe('makeThirdPartyServiceRequest', () => {
it('should make a request with correct parameters', async () => {
// Given
const url = `https://someurltoathirdpartyservice`
const params = 'any params'
const apiResponse = 'any response'
mocked(helpers.makeRequest).mockResolvedValue(apiResponse)
// When
const actual = await thirdPartyService.makeThirdPartyServiceRequest(params)
// Then
expect(actual).toBe(apiResponse)
expect(helpers.makeRequest).toHaveBeenCalledWith(
url,
{
method: 'POST',
body: params
}
)
})
})
})
hoangdv answer is also valid, but i will answer as well how i continued.
if you completely want to mock middy you mock like following:
jest.mock('#middy/core', () => {
return (handler) => {
return {
use: jest.fn().mockImplementation(() => {
// ...use(ssm()) will return handler function
return {
before: jest.fn().mockReturnValue(handler)
}
})
}
}
})
However if you dont want to completely mock middy, you can instead mock the async getInternal function from middy/util called in before like this:
jest.doMock('#middy/util', () => ({
...(jest.requireActual('#middy/util') as {}),
getInternal: jest.fn()
}))
import { getInternal } from '#middy/util'
and then in the test
describe('thirdPartyService()', () => {
beforeEach(() => {
jest.spyOn(helpers, 'makeRequest') // spy on helpers unit
})
describe('makeThirdPartyServiceRequest', () => {
it('should make a request with correct parameters', async () => {
// Given
const url = `https://someurltoathirdpartyservice`
const params = 'any params'
const apiResponse = 'any response'
mocked(getInternal).mockResolvedValue({
twilioSecrets: { accountSid: 'someSID', serviceId:
'someServiceID', token: 'someToken' }
})
mocked(helpers.makeRequest).mockResolvedValue(apiResponse)
// When
const actual = await thirdPartyService.makeThirdPartyServiceRequest(params)
// Then
expect(actual).toBe(apiResponse)
expect(helpers.makeRequest).toHaveBeenCalledWith(
url,
{
method: 'POST',
body: params
}
)
})
})
})
this will mock the async part of middy.
I have a todo app. Im trying to use context api(first time). I have add, delete and get functions in context. I can use add and delete but cant return the get response to state. It returns promise if i log; context. Im using async await. I tried almost everything i know but cant solve it. Where is my fault ?
Thank you.
task-context.js
import React, { useReducer } from "react";
import TaskContext from "./task-actions";
import { TaskReducer, ADD_TASK, GET_TASKS, REMOVE_TASK } from "./reducers";
const GlobalState = (props) => {
const [tasks, dispatch] = useReducer(TaskReducer, { tasks: [] });
const addTask = (task) => {
dispatch({ type: ADD_TASK, data: task });
};
const removeTask = (taskId) => {
dispatch({ type: REMOVE_TASK, data: taskId });
};
const getTasks = () => {
dispatch({ type: GET_TASKS });
};
return (
<TaskContext.Provider
value={{
tasks: tasks,
getTasks: getTasks,
addTask: addTask,
removeTask: removeTask,
}}
>
{props.children}
</TaskContext.Provider>
);
};
export default GlobalState;
reducers.js
import taskService from "../Services/tasks-service";
export const ADD_TASK = "ADD_TASK";
export const GET_TASKS = "GET_TASKS";
export const REMOVE_TASK = "REMOVE_TASK";
const addTask = async (data, state) => {
console.log("Adding : " + data.title);
try {
let task = {
title: data.title,
description: data.description,
comment: data.comment,
progress: data.status
};
const res = await taskService.addNewTask(task);
console.log(res);
if (res) {
getTasks();
}
} catch (err) {
console.log(err);
}
return;
};
const getTasks = async () => {
let response = {}
try {
const res = await taskService.loadTasks();
response = res.data
} catch (err) {
console.log(err);
}
return { tasks: response }
};
const removeTask = async (data) => {
try {
await taskService.deleteTask(data.id);
} catch (err) {
console.log(err);
}
};
export const TaskReducer = (state, action) => {
switch (action.type) {
case ADD_TASK:
return addTask(action.data);
case GET_TASKS:
console.log(getTasks());
return getTasks();
case REMOVE_TASK:
return removeTask(action.data);
default:
return state;
}
};
task-actions.js
import React from "react";
export default React.createContext({
addTask: (data) => {},
removeTask: (data) => {},
getTasks: () => {}
});
To start with, you are getting promises returned because you are explicitly returning promises: return addTask(action.data). All your actions are returning promises into the reducer.
A reducer should be a pure function, meaning that it does not have any side effects (call code outside its own scope), or contain any async functionality, and it should return the same data given the same inputs every single time. You've essentially got the workflow back to front.
There's a lot to unpick here so I'm going to provide pseudocode rather than try and refactor the entire service, which you will have a more complete understanding of. Starting with the reducer:
export const TaskReducer = (state, action) => {
switch (action.type) {
case ADD_TASK:
return [...state, action.data];
case GET_TASKS:
return action.data;
case REMOVE_TASK:
return state.filter(task => task.id !== action.data.id);
default:
return state;
}
};
This reducer describes how the state is updated after each action is complete. All it should know how to do is update the state object/array it is in charge of. When it comes to fetching data, calling the reducer should be the very last thing you have to do.
Now on to the actions. The add action is a problem because its not actually returning any data. On top of that, it calls getTasks when really all it ought to do is return one added task (which should be getting returned from await taskService.addNewTask). I would expect that res.data is actually a task object, in which case:
export const addTask = async (data) => {
try {
const task = {
title: data.title,
description: data.description,
comment: data.comment,
progress: data.status
};
const res = await taskService.addNewTask(task);
return res.data;
} catch (err) {
return err;
}
};
Similarly for getTasks, I'm going to assume that await taskService.loadTasks returns an array of task objects. In which case, we can simplify this somewhat:
export const getTasks = async () => {
try {
const res = await taskService.loadTasks();
return res.data;
} catch (err) {
return err;
}
};
Your removeTask action is essentially fine, although you will want to return errors instead of just logging them.
Notice we're now exporting these actions. That is so we can now call them from within GlobalState. We're running into issues with name collision so I've just underscored the imported actions for demo purposes. In reality, it might be better to move all the functionality we did in the last step into your taskService, and import that straight into GlobalState instead. Since that's implementation specific I'll leave it up to you.
import {
TaskReducer,
ADD_TASK,
GET_TASKS,
REMOVE_TASK,
addTask as _addTask,
getTasks as _getTasks,
removeTask as _removeTask,
} from "./reducers";
const GlobalState = (props) => {
const [tasks, dispatch] = useReducer(TaskReducer, { tasks: [] });
const addTask = async (task) => {
const added = await _addTask();
if (added instanceof Error) {
// handle error within the application
return;
};
dispatch({ type: ADD_TASK, data: added });
};
const removeTask = async (taskId) => {
const removed = await _removeTask(taskId);
if (removed instanceof Error) {
// handle error within the application
return;
};
dispatch({ type: REMOVE_TASK, data: taskId });
};
const getTasks = async () => {
const tracks = await _getTracks();
if (tracks instanceof Error) {
// handle error within the application
return;
};
dispatch({ type: GET_TASKS, data: tracks });
};
...
}
Hopefully now you can see how the workflow is supposed to progress. First we call for data from our backend or other API, then we handle the response within the application (for instance, dispatching other actions to notify about errors or side effects of the new data) and then finally dispatch the new data into our state.
As stated at the beginning, what I've provided is essentially pseudocode, so don't expect it to work out of the box.
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;
}
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())
})
})
})