I am having trouble mocking a function inside a module. (Not sure if this is possible.
So I have a module called myActions.ts
import * as Config from "../utils/Config";
const _config = Config.getConfig();
export const requestLoadDataA = (postLoadAction: Function = undefined) => {
return async function (dispatch, getState) {
const client = { url: `${_config.ApiUrl}getDataA` };
...
}
}
This module contains a Config.getConfig() and this is what I want to mock.
Config module looks like this:
export const getConfig = () => {
const app = document.getElementById("react-app");
if (app) {
const config = app.dataset.configuration;
return JSON.parse(config) as IConfiguration;
} else {
return undefined;
}
};
This is the jest test I have so far and it doesn't work:
describe("DATA_A Action Creator (Sync): Tests", () => {
afterEach(fetchMock.restore);
it("REQUEST and SUCCESS actions on successful loadData()", () => {
const dataA: any = require("../../__mockData__/dataA.json");
fetchMock.mock("/getDataA", {
status: 200,
body: dataA
});
const _config = { };
const spy = jest.spyOn(Config, "getConfig");
spy.mockReturnValue(_config);
const store = mockStore({
dataA: {
hasLoadedEntities: false,
isLoadingEntities: false
}
});
return store.dispatch(aActions.requestLoadDataA())
.then(() => {
const expectedActions = store.getActions();
expect(expectedActions.length).toEqual(2);
expect(expectedActions).toContainEqual({ type: ACTION_TYPES.LOAD_A_REQUESTED });
expect(expectedActions).toContainEqual({ type: ACTION_TYPES.LOAD_A_SUCCESS, data: resultData });
});
});
}
I get a "cannot read property 'ApiUrl' of undefined.
How can I mock the _config.ApiUrl object?
Not sure if I understood you.
you can mock your default import like this
import * as Config from "../utils/Config";
jest.mock("../utils/Config", () => ({
getConfig: () => ({ ApiUrl: 'yourMockApiUrl' })
}));
Related
I have this function inside a helper:
export const useDAMProductImages = (imageId: string) => {
const {
app: { baseImgDomain },
} = getConfig();
const response: MutableRefObject<string[]> = useRef([]);
useEffect(() => {
const getProductImages = async (imageId: string) => {
try {
const url = new URL(FETCH_URL);
const res = await fetchJsonp(url.href, {
jsonpCallbackFunction: 'callback',
});
const jsonData = await res.json();
response.current = jsonData;
} catch (error) {
response.current = ['error'];
}
};
if (imageId) {
getProductImages(imageId);
}
}, [imageId]);
return response.current;
};
In test file:
import .....
jest.mock('fetch-jsonp', () =>
jest.fn().mockImplementation(() =>
Promise.resolve({
status: 200,
json: () => Promise.resolve({ set: { a: 'b' } }),
}),
),
);
describe('useDAMProductImages', () => {
beforeEach(() => {
jest.clearAllMocks();
cleanup();
});
it('should return empty array', async () => {
const { result: hook } = renderHook(() => useDAMProductImages('a'), {});
expect(hook.current).toMatchObject({ set: { a: 'b' } });
});
});
The problem is that hook.current is an empty array. Seems that useEffect is never called. Can someone explain to me what I'm doing wrong and how I should write the test? Thank you in advance
I have this functional React component:
// CreateNotification.tsx
import {useMutation} from '#apollo/client';
import resolvers from '../resolvers';
const createNotification = (notification) => {
const [createNotification] = useMutation(resolvers.mutations.CreateNotification);
createNotification({
variables: {
movie_id: notification.movie.id,
actor_id: notification.user.id,
message:
`${notification.user.user_name} has added ${notification.movie.original_title} to their watchlist.`,
},
});
};
export default createNotification;
I call the createNotification component in a function and pass in some variables after a other useMutation hook has been called:
// AddMovie.tsx
const addMovie = async (movie: IMovie) => {
await addUserToMovie({
variables: {...movie, tmdb_id: movie.id},
update: (cache, {data}) => {
cache.modify({
fields: {
moviesFromUser: () => {
return [...data.addUserToMovie];
},
},
});
},
}).then( async () => {
createNotification({movie: movie, user: currentUserVar()});
});
};
When I run the code I get the (obvious) error:
Uncaught (in promise) Error: Invalid hook call. Hooks can only be called inside of the body of a function component
Because I call the createNotification hook in the addMovie function.
If I move the createNotification to the top of the component:
// AddMovie.tsx
const AddMovieToWatchList = ({movie}: {movie: IMovie}) => {
createNotification({movie: movie, user: currentUserVar()});
const [addUserToMovie] = useMutation(resolvers.mutations.AddUserToMovie);
const addMovie = async (movie: IMovie) => {
await addUserToMovie({
variables: {...movie, tmdb_id: movie.id},
update: (cache, {data}) => {
cache.modify({
fields: {
moviesFromUser: () => {
return [...data.addUserToMovie];
},
},
});
},
});
};
}
The code works fine, except that the hook is now called every time the AddMovie component is rendered instead of when the addMovie function is called from the click:
return (
<a className={classes.addMovie} onClick={() => addMovie(movie)}>
Add movie to your watchlist
</a>
);
Figured it out:
// createNotification.tsx
import {useMutation} from '#apollo/client';
import resolvers from '../resolvers';
export const createNotification = () => {
const [createNotification, {data, loading, error}] = useMutation(resolvers.mutations.CreateNotification);
const handleCreateNotification = async (notification) => {
createNotification({
variables: {
movie_id: notification.movie.id,
actor_id: notification.user.id,
message:
`${notification.user.user_name} has added ${notification.movie.original_title} to their watchlist.`,
},
});
console.log(data, loading, error);
};
return {
createNotification: handleCreateNotification,
};
};
If I'm correct then this returns a reference (createNotification) to the function handleCreateNotification
Then in the component I want to use the createNotification helper I import it:
// AddMovie.tsx
import {createNotification} from '../../../../helpers/createNotification';
const AddMovieToWatchList = ({movie}: {movie: IMovie}) => {
const x = createNotification();
const addMovie = async (movie: IMovie) => {
await addUserToMovie({
variables: {...movie, tmdb_id: movie.id},
update: (cache, {data}) => {
cache.modify({
fields: {
moviesFromUser: () => {
return [...data.addUserToMovie];
},
},
});
},
}).then( async () => {
x.createNotification({movie: movie, user: currentUserVar()});
});
}
};
You (kind of) answer your own question by showing the error and saying it's obvious. createNotification is not a React component, and it is not a custom hook, it is just a function. Thus using a hook inside of it breaks the Rules of Hooks.
If you want to keep that logic in it's own function, that's fine, just redefine your component like this:
const AddMovieToWatchList = ({movie}: {movie: IMovie}) => {
const [addUserToMovie] = useMutation(resolvers.mutations.AddUserToMovie);
const [createNotification] = useMutation(resolvers.mutations.CreateNotification);
const addMovie = async (movie: IMovie) => {
await addUserToMovie({
variables: {...movie, tmdb_id: movie.id},
update: (cache, {data}) => {
cache.modify({
fields: {
moviesFromUser: () => {
return [...data.addUserToMovie];
},
},
});
},
});
await createNotification({movie: movie, user: currentUserVar()});
};
return (
<a className={classes.addMovie} onClick={() => addMovie(movie)}>
Add movie to your watchlist
</a>
);
}
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));
});
})
Here is my custom hook:
export function useClientRect() {
const [scrollH, setScrollH] = useState(0);
const [clientH, setClientH] = useState(0);
const ref = useCallback(node => {
if (node !== null) {
setScrollH(node.scrollHeight);
setClientH(node.clientHeight);
}
}, []);
return [scrollH, clientH, ref];
}
}
I want each time that it is called, it return my values. like:
jest.mock('useClientRect', () => [300, 200, () => {}]);
How can I achieve this?
Load the hook as a module. Then mock the module:
jest.mock('module_name', () => ({
useClientRect: () => [300, 200, jest.fn()]
}));
mock should be called on top of the file outside test fn. Therefore we are going to have only one array as the mocked value.
If you want to mock the hook with different values in different tests:
import * as hooks from 'module_name';
it('a test', () => {
jest.spyOn(hooks, 'useClientRect').mockImplementation(() => ([100, 200, jest.fn()]));
//rest of the test
});
Adding on to this answer for typescript users encountering the TS2339: Property 'mockReturnValue' does not exist on type error message. There is now a jest.MockedFunction you can call to mock with Type defs (which is a port of the ts-jest/utils mocked function).
import useClientRect from './path/to/useClientRect';
jest.mock('./path/to/useClientRect');
const mockUseClientRect = useClientRect as jest.MockedFunction<typeof useClientRect>
describe("useClientRect", () => {
it("mocks the hook's return value", () => {
mockUseClientRect.mockReturnValue([300, 200, () => {}]);
// ... do stuff
});
it("mocks the hook's implementation", () => {
mockUseClientRect.mockImplementation(() => [300, 200, () => {}]);
// ... do stuff
});
});
Well, this is quite tricky and sometimes developers get confused by the library but once you get used to it, it becomes a piece of cake. I faced a similar issue a few hours back and I'm sharing my solution for you to derive your solution easily.
My custom Hook:
import { useEffect, useState } from "react";
import { getFileData } from "../../API/gistsAPIs";
export const useFilesData = (fileUrl: string) => {
const [fileData, setFileData] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
useEffect(() => {
setLoading(true);
getFileData(fileUrl).then((fileContent) => {
setFileData(fileContent);
setLoading(false);
});
}, [fileUrl]);
return { fileData, loading };
};
My mock code:
Please include this mock in the test file outside of your test function.
Note: Be careful about the return object of mock, it should match with the expected response.
const mockResponse = {
fileData: "This is a mocked file",
loading: false,
};
jest.mock("../fileView", () => {
return {
useFilesData: () => {
return {
fileData: "This is a mocked file",
loading: false,
};
},
};
});
The complete test file would be:
import { render, screen, waitFor } from "#testing-library/react";
import "#testing-library/jest-dom/extend-expect";
import FileViewer from "../FileViewer";
const mockResponse = {
fileData: "This is a mocked file",
loading: false,
};
jest.mock("../fileView", () => {
return {
useFilesData: () => {
return {
fileData: "This is a mocked file",
loading: false,
};
},
};
});
describe("File Viewer", () => {
it("display the file heading", async () => {
render(<FileViewer fileUrl="" filename="regex-tutorial.md" className="" />);
const paragraphEl = await screen.findByRole("fileHeadingDiplay");
expect(paragraphEl).toHaveTextContent("regex-tutorial.md");
});
}
I am trying to test if a button dispatches an action but instead of an action type I get []. The functionality works completely fine outside the test this is why I don't understand why the test fails.
My test:
const mockStore = configureStore();
const store = mockStore({});
describe('buttonUploadWorks', () => {
it('checks if the button for upload dispatches an action', () => {
const wrapper = shallow(
<Provider store = {store}>
<DropzoneComponent.WrappedComponent/>
</Provider>).dive();
const uploadButton = wrapper.find('button').at(0);
uploadButton.simulate('onClickUpload');
const expectedAction = UPLOAD_STARTING;
wrapper.setState({ accepted: ['abc'] });
const action = store.getActions();
expect(action).to.equal(expectedAction);
});
});
My actions:
export const uploadStarting = () => {
return {type: UPLOAD_STARTING};
};
export const uploadSuccess = uploadMessage => {
return {type: UPLOAD_SUCCESS, uploadMessage};
};
export const uploadFail = error => {
return {type: UPLOAD_FAIL, error};
};
export function tryUpload (file) {
const formData = new FormData();
formData.append('file', file);
return (dispatch) => {
dispatch(uploadStarting());
axios.post(filesEndpoint, formData).then(function (response) {
dispatch(uploadSuccess(response));
}).catch(function (error) {
dispatch(uploadFail(error));
});
};
};
And the button:
<button className='buttonUpload' onClick={() => { this.state.accepted.length > 0 ? this.onClickUpload().bind(this) : alert('No files presented'); }}>UPLOAD</button>
onClickUpload() {
this.props.dispatch(tryUpload(this.state.accepted[0]));
localStorage.clear();
this.setState({accepted: []});
this.props.history.push(searchPath);
}
That's happening because setState({accepted:[]}) triggers before this.props.dispatch(tryUpload(this.state.accepted[0])) you could fix it binding dispatch function inside a promise function and then calling the setState function.
JavaScript Promises