Jest testing service call responses witth promises, useEffect and useState hooks - reactjs

I'm having some difficulty testing this useEffect in jest. The following piece of code is within a react functional component and I want to return some mock values when the serviceFn is called. The returned data is written back to state.
//from service-factory.js
const serviceFn = () => (
({ personId }) => (
ionXHR.request(
`/persons/${personId}/`,
'GET',
null,
'json',
)
)
);
//from Component.jsx
const service = useRef(serviceFn());
useEffect(() => {
service.current({ personId:123456 }).then((response) => {
if (response.data) {
setData(response.data);
setLoadingState('SUCCESS');
} else {
setLoadingState('FAILED');
}
});
}, [personId]);
I have the following, but not sure what else I would need.
function mockReturnFn() { return 'Test'; }
const wrapper = mount(<Component/>);
const somethingSpy = jest.spyOn(wrapper, 'serviceFn').mockImplementation(mockReturnFn);
Update:
So, I think I'm getting close.
In my test file I had to import the function
import { serviceFn } from './service-factory';
jest.mock('./service-factory', () => ({ serviceFn : jest.fn() }));
In my test I have
serviceFn.mockImplementation(() => Promise.resolve('test1234'));
The issue now with this is I am getting service.current is not a function
I tried to do this but now getting _serviceFactory.serviceFn.mockImplementation is not a function
jest.mock('./service-factory', () => (
{
serviceFn: {
current: jest.fn(),
},
}
));

serviceFn is factory function, it returns a function that makes requests.
Considering it's named export, it should be initially stubbed as:
jest.mock('./service-factory', () => {
const mockService = jest.fn();
return {
__esModule: true,
mockService,
serviceFn: jest.fn().mockReturnValue(mockService)
}
});
mockService is exposed and allows to mock specific responses:
mockService.mockResolvedValue({ data: ... });
Since it's basic wrapper over ionXHR, it's also possible to mock responses one level higher on ionXHR.request calls.

Related

Testing iron-session `withIronSessionSsr` that uses `context` in NextJs, React, and Jest

So I have this NextJs utility file function below that returns an imported function. Then that imported function accepts a handler, that uses context, as an argument.
import { ironSessionUtil } from './anotherUtil'
const checker = () =>
ironSessionUtil(({ req }) => {
const { user } = req.session
if (!user) {
return {
redirect: { destination: '/welcome', permanent: false }
}
}
return { props: { details: user.details } }
})
export default checker
So far I've tried just calling the method but it returns an Async Function.
it('should return props', () => {
const result = checker()
console.log('result >> ', result)
})
This returns:
[AsyncFunction: nextGetServerSidePropsHandlerWrappedWithIronSession]
I've been scouring the web on how to test this. Would you suggest refactors to make this more testable? Or do you know how this can be done in Jest?

mocking my fetch function does not work, keeps getting back undefined

I am trying to mock a simple function that uses fetch. The function in question looks like this:
export const getPokemon = async () => {
//function that makes the API call and fetches our pokemon
//getPokemon.js
const randomId = () => Math.floor(Math.random() * 151 + 1);
const pokemonApiUrl = `https://pokeapi.co/api/v2/pokemon/`;
export const getPokemon = async () => {
//function that makes the API call and fetches our pokemon
const id = randomId();
let pokemon = { name: "", image: "" };
try {
const result = await fetch(`https://pokeapi.co/api/v2/pokemon/${id}`);
console.log(result)
const data = await result.json();
pokemon.name = data.name;
pokemon.image = data.sprites.other["official-artwork"].front_default;
return pokemon;
} catch (err) {
console.error(err);
Whenever I try to mock the function in my unit tests I receive back a TypeError: Cannot read property 'json' of undefined. Basically, the result comes back as undefined and thus we cannot call our .json(). It works fine in production and the fetch calls work as expected. I am using React Testing Library and Jest.
I have tried to replaced the global fetch in the following manner:
//PokemonPage.test.js
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ data: { name: 'Charizard' } }),
})
);
I've also tried to create a fakeFetch and send it in to my function as a dependency injection, but I get the exact same error.
Lastly, I've tried to install jest-fetch-mock but yet again I am getting the same error... Has anyone encountered the same thing?
The failing function gets called in production here:
function Pokemon({ pokemonTrainer }) {
...
useEffect(() => {
async function fetchData() {
pokemonRef.current = await getPokemon();
setPokemonList((prev) => [
...prev,
{ name: pokemonRef.current.name, image: pokemonRef.current.image },
]);
}
fetchData();
}, []);
...
}

How to mock async call in React functional component using jest

I am testing a functional component that has a submit button that makes an async call to an api. The async call is located within a custom hook. As per standard testing practices, I have mocked the hook, so that my mock will be called instead of the actual async api:
someComponent.test.js
jest.mock("../../../CustomHooks/user", () => ({
useUser: () => ({
error: null,
loading: false,
forgotPassword: <SOMETHING HERE>
})
}));
I know that my forgotPassword function is called because when I change it to forgotPassword: "", I get an error in my test stating that forgotPassword is not a function.
A very simple representation of the function that is called when my submit button is clicked is this:
someComponent.js
import { useUser } from "../../../CustomHooks/user"
const SomeComponent = () => {
....state and other things etc....
const { error, loading, forgotPassword } = useUser()
const submit = async () => {
await forgotPassword(emailValue);
setState(prevState => {
return {
...prevState,
content: "code"
};
});
}
}
NOTE: My call to the async function await forgotPassword... is wrapped in a try/catch block in my code, but I have left this out for clarity.
In production, when the submit button is pressed, the async call occurs, and then the state should be switched, thus rendering some other components. My test looks to see if these components have been rendered (I am using react testing library for this).
The problem that I am having is that no matter what I place in the placeholder of the first code block, my test will always fail as the setState block is never reached. If I remove the await statement, then the setState block is hit and the component that I want to appear is there as the state has changed. However, obviously this will not work as intended outside of the test as the actual call is asynchronous. Here are some of the approaches that I have tried that do not work:
DOESN'T WORK
forgotPassword: () => {
return Promise.resolve({ data: {} });
}
DOESN'T WORK
forgotPassword: jest.fn(() => {
return Promise.resolve();
})
DOESN'T WORK
forgotPassword: jest.fn(email => {
return new Promise((resolve, reject) => {
if (email) {
resolve(email);
} else {
reject("Error");
}
});
}),
As I have said already, if I remove the await statement, then the state changes and the component appears, and hence the test passes. However, for obvious reasons, this is not what I want.
Extra Info
Here is a simplified version of my test:
it("changes state/content from email to code when submit clicked", () => {
const { getByTestId, getByText, debug } = render(<RENDER THE COMPONENT>);
const submitButton = getByTestId("fpwSubmitButton");
expect(submitButton).toBeInTheDocument();
const emailInput = getByTestId("fpwEmailInput");
fireEvent.change(emailInput, {
target: { value: "testemail#testemail.com" }
});
fireEvent.click(submitButton);
debug();
THE STATEMENTS BELOW ARE WHERE IT FAILS AS THE STATE DOESN'T CHANGE WHEN AWAIT IS PRESENT
const codeInput = getByTestId("CodeInput");
expect(codeInput).toBeInTheDocument();
});
To anyone who encounters this same problem, I found three ways that this can be solved (the preferred method is Option 3). All methods use a simple mock function that replaces the <SOMETHING HERE> of the first code block in my question. This can be replaced with () => {}:
jest.mock("../../../CustomHooks/user", () => ({
useUser: () => ({
error: null,
loading: false,
forgotPassword: () => {}
})
}));
Option 1
The first approach is to wrap your test code that relies on an async function in a setTimeout with a done callback:
it("changes state/content from email to code when submit clicked", done => {
const { getByTestId, debug } = render(<RENDER THE COMPONENT>);
const submitButton = getByTestId("fpwSubmitButton");
expect(submitButton).toBeInTheDocument();
const emailInput = getByTestId("fpwEmailInput");
fireEvent.change(emailInput, {
target: { value: "testemail#testemail.com" }
});
fireEvent.click(submitButton);
setTimeout(() => {
const codeInput = getByTestId("CodeInput");
expect(codeInput).toBeInTheDocument();
done();
});
debug();
});
Notice on the top line the done call back, as well as the test code wrapped in setTimeout at the bottom, and then invoking the callback within the setTimeout to tell jest that the test is done. If you don't call the done callback, the test will fail as it will timeout.
Option 2
The second approach is to use a function called flushPromises():
function flushPromises() {
return new Promise(resolve => setImmediate(resolve));
}
it("changes state/content from email to code when submit clicked", async () => {
const { getByTestId, debug } = render(<RENDER THE COMPONENT>);
const submitButton = getByTestId("fpwSubmitButton");
expect(submitButton).toBeInTheDocument();
const emailInput = getByTestId("fpwEmailInput");
fireEvent.change(emailInput, {
target: { value: "testemail#testemail.com" }
});
fireEvent.click(submitButton);
await flushPromises();
const codeInput = getByTestId("CodeInput");
expect(codeInput).toBeInTheDocument();
debug();
});
Notice the flushPromises() function at the top, and then the call site towards the bottom.
Option 3 (Preferred Method)
The final method is to import wait from react-testing-library, set your test as asynchronous and then await wait() whenever you have async code:
...
import { render, fireEvent, cleanup, wait } from "#testing-library/react";
...
it("changes state/content from email to code when submit clicked", async () => {
const { getByTestId, debug } = render(<RENDER THE COMPONENT>);
const submitButton = getByTestId("fpwSubmitButton");
expect(submitButton).toBeInTheDocument();
const emailInput = getByTestId("fpwEmailInput");
fireEvent.change(emailInput, {
target: { value: "testemail#testemail.com" }
});
fireEvent.click(submitButton);
await wait()
const codeInput = getByTestId("CodeInput");
expect(codeInput).toBeInTheDocument();
debug();
});
All of these solutions work because they wait for the next event loop before executing the test code. Wait() is basically a wrapper around flushPromises() with the added benefit of including act(), which will help to silence test warnings.
try something like this
forgotPassword: jest.fn( async email => {
return await new Promise( ( resolve, reject ) => {
if ( email ) {
resolve( email );
} else {
reject( "Error" );
}
} );
} );
If it doesn't work let me know.

Jest how to mock api call

I am trying to mock my api call with jest but for some reason it's not working. I don't really understand why. Anyone has an idea?
(the test keep call the original api call function and not the mock)
my test.js
import { getStuff } from '../stuff';
import * as api from '../../util/api';
describe('Action getStuff', () => {
it('Should call the API to get stuff.', () => {
api.call = jest.fn();
getStuff('slug')(() => {}, () => {});
expect(api.call).toBeCalled();
jest.unmock('../../util/api.js');
});
});
stuff.js redux action
import api from '#util/api';
import { STUFF, API } from '../constant';
export const getStuff = slug => (dispatch, getState) => {
const state = getState();
api.call(API.STUFF.GET, (err, body) => {
if (err) {
console.error(err.message);
} else {
dispatch({
type: STUFF.GET,
results: body,
});
}
}, {
params: { slug },
state
});
};
The import are immutable so it won't work, what you should is mock the whole module. Either with a __mock__ directory or simply with:
jest.mock('../../util/api');
const { call } = require('../../util/api');
call.mockImplementation( () => console.log("some api call"));

How to unit test Promise catch() method behavior with async/await in Jest?

Say I have this simple React component:
class Greeting extends React.Component {
constructor() {
fetch("https://api.domain.com/getName")
.then((response) => {
return response.text();
})
.then((name) => {
this.setState({
name: name
});
})
.catch(() => {
this.setState({
name: "<unknown>"
});
});
}
render() {
return <h1>Hello, {this.state.name}</h1>;
}
}
Given the answers below and bit more of research on the subject, I've come up with this final solution to test the resolve() case:
test.only("greeting name is 'John Doe'", async () => {
const fetchPromise = Promise.resolve({
text: () => Promise.resolve("John Doe")
});
global.fetch = () => fetchPromise;
const app = await shallow(<Application />);
expect(app.state("name")).toEqual("John Doe");
});
Which is working fine. My problem is now testing the catch() case. The following didn't work as I expected it to work:
test.only("greeting name is 'John Doe'", async () => {
const fetchPromise = Promise.reject(undefined);
global.fetch = () => fetchPromise;
const app = await shallow(<Application />);
expect(app.state("name")).toEqual("<unknown>");
});
The assertion fails, name is empty:
expect(received).toEqual(expected)
Expected value to equal:
"<unknown>"
Received:
""
at tests/components/Application.spec.tsx:51:53
at process._tickCallback (internal/process/next_tick.js:103:7)
What am I missing?
The line
const app = await shallow(<Application />);
is not correct in both tests. This would imply that shallow is returning a promise, which it does not. Thus, you are not really waiting for the promise chain in your constructor to resolve as you desire. First, move the fetch request to componentDidMount, where the React docs recommend triggering network requests, like so:
import React from 'react'
class Greeting extends React.Component {
constructor() {
super()
this.state = {
name: '',
}
}
componentDidMount() {
return fetch('https://api.domain.com/getName')
.then((response) => {
return response.text()
})
.then((name) => {
this.setState({
name,
})
})
.catch(() => {
this.setState({
name: '<unknown>',
})
})
}
render() {
return <h1>Hello, {this.state.name}</h1>
}
}
export default Greeting
Now we can test it by calling componentDidMount directly. Since ComponentDidMount is returning the promise, await will wait for the promise chain to resolve.
import Greeting from '../greeting'
import React from 'react'
import { shallow } from 'enzyme'
test("greeting name is 'John Doe'", async () => {
const fetchPromise = Promise.resolve({
text: () => Promise.resolve('John Doe'),
})
global.fetch = () => fetchPromise
const app = shallow(<Greeting />)
await app.instance().componentDidMount()
expect(app.state('name')).toEqual('John Doe')
})
test("greeting name is '<unknown>'", async () => {
const fetchPromise = Promise.reject(undefined)
global.fetch = () => fetchPromise
const app = shallow(<Greeting />)
await app.instance().componentDidMount()
expect(app.state('name')).toEqual('<unknown>')
})
By the looks of this snippet
.then((response) => {
return response.text();
})
.then((name) => {
this.setState({
name: name
});
})
it seems that text would return a string, which then would appear as the name argument on the next 'then' block. Or does it return a promise itself?
Have you looked into jest's spyOn feature? That would help you to mock not only the fetch part but also assert that the setState method was called the correct amount of times and with the expected values.
Finally, I think React discourages making side effects inside constructor. The constructor should be used to set initial state and other variables perhaps. componentWillMount should be the way to go :)
Recently, I have faced the same issue and ended up resolving it by following way
(taking your code as an example)
test.only("greeting name is 'John Doe'", async () => {
const fetchPromise = Promise.resolve(undefined);
jest.spyOn(global, 'fetch').mockRejectedValueOnce(fetchPromise)
const app = await shallow(<Application />);
await fetchPromise;
expect(app.state("name")).toEqual("<unknown>");});
Another way if you don't want to call done then return the next promise state to jest. Based on result of assertion( expect ) test case will fail or pass.
e.g
describe("Greeting", () => {
test("greeting name is unknown", () => {
global.fetch = () => {
return new Promise((resolve, reject) => {
process.nextTick(() => reject());
});
};
let app = shallow(<Application />);
return global.fetch.catch(() => {
console.log(app.state());
expect(app.state('name')).toBe('<unknown>');
})
});
});

Resources