Mocking a promise inside useEffect with CRA, React Testing Library and Jest - reactjs

I am having an issue mocking a returned Promise using:
Create React app
Jest
RTL
I have a file:
const books = [{
id: 1,
name: 'book'
}];
export const getBooks = () =>
new Promise((res) => res(books));
I have a useEffect in my app:
export const App = () => {
const [books, setBooks] = useState(undefined);
useEffect(() => {
const fetchData = async () => {
try {
const response = await getBooks();
setBooks(response);
} catch (error) {
setError("There seems to be an issue. Error:", error);
}
};
fetchData();
}, []);
return (
<div>
{books &&
books.map((book) => {
return (
<li key={book.id}>
{book.name}
</li>
);
})
}
</div>
I have a test:
import { App } from './App';
import { getBooks } from './books';
jest.mock('./books', () => ({
getBooks: jest.fn(),
}));
getBlocks.mockReturnValue(() => Promise.resolve([{
id: 1,
name: 'mock book'
}]));
describe('App', () => {
it('should render blocks', async () => {
await render(<App />);
expect(screen.getByText('mock book')).toBeInTheDocument();
});
});
I just can't mock the return value! I can assert it's been called and I can console log the getBooks to see that it's mocked I just can't get any results. I also want to reject it so I can test the unhappy path but it won't work. Any ideas?

Few things:
You have a typo, it's not getBlocks but getBooks.
The await keyword is not necessary before rendering the component with render.
getBooks returns a promise that resolves with the value of books, yet when you're trying to mock it, you are making it return a function that returns a promise. Very different things.
You have to move the mocking to the test block in which it'll be used, or if you need this mocked value from getBooks on each one of your tests, you can move it inside a beforeEach hook. You can always override it for a specific test in which you are testing some edge case (e.g. an exception being thrown by the function, A.K.A "Unhappy path").
On the component's first render, books will be undefined, so you need to wait for the state to be updated. getByText query won't work, since it will immediately throw an error because it won't find the text you're expecting. You need to use the findByText query for this. It returns a promise that resolves when an element that matches the given query is found and rejects if the element is not found after the default timeout of 1000ms.
Since getBooks returns a promise, it makes more sense to use mockResolvedValue instead of mockReturnValue.
import { render, screen } from "#testing-library/react"
import { App } from "./App"
import { getBooks } from "./books"
jest.mock("./books", () => ({
getBooks: jest.fn()
}))
describe("App", () => {
it("should render blocks", async () => {
getBooks.mockResolvedValueOnce([{ id: 1, name: "mock book" }])
render(<App />)
expect(await screen.findByText("mock book")).toBeInTheDocument()
})
})

Try this:
jest.mock('./books', () => ({
getBooks: jest.fn().mockReturnValue(() => Promise.resolve([{
id: 1,
name: 'mock book'
}]));
}));

Related

React test if something hasn't happened after useEffect

I'm using react-testing-library and jest to test if my API is not invoked when a certain prop is set. Currently the test succeeds immediately without finishing the useEffect() call. How do I make the test wait until useEffect has finished, so I can be certain the API has not been called?
The code:
const MyComponent = ({ dontCallApi }) => {
React.useEffect(() => {
const asyncFunction = async () => {
if (dontCallApi) {
return
}
await callApi()
}
asyncFunction
}, [])
return <h1>Hi!</h1>
}
it('should not call api when dontCallApi is set', async () => {
const apiSpy = jest.spyOn(api, 'callApi')
render(<MyComponent dontCallApi />)
expect(apiSpy).not.toHaveBeenCalled()
})
In your case, you could spy on React.useEffect and provide an alternative implementation. jest.spyOn(React, "useEffect").mockImplementation((f) => f())
so now you dont't have to care about the handling of useEffect anymore.
If you also want to test useEffect in a descent way you may extract the logic in a custom hook and use the testing library for hooks with the renderHooks function to test your use case.
I would test your Component like this:
import React from "react";
import { MyComponent } from "./Example";
import { render } from "#testing-library/react";
import { mocked } from "ts-jest/utils";
jest.mock("./api", () => ({
callApi: jest.fn(),
}));
import api from "./api";
const mockApi = mocked(api);
jest.spyOn(React, "useEffect").mockImplementation((f) => f());
describe("MyComponet", () => {
afterEach(() => {
jest.clearAllMocks();
});
it("should not call api when dontCallApi is set", async () => {
render(<MyComponent dontCallApi />);
expect(mockApi.callApi).toHaveBeenCalledTimes(0);
});
it("should call api when is not set", async () => {
render(<MyComponent />);
expect(mockApi.callApi).toHaveBeenCalledTimes(1);
});
});
Edit 03.07.2020
I recently found out that there is a possibility to query something like you wanted without mocking useEffect. You could simply use the async utilities of react testing library and get the following:
import React from "react";
import { MyComponent } from "./TestComponent";
import { render, waitFor } from "#testing-library/react";
import { api } from "./api";
const callApiSpy = jest.spyOn(api, "callApi");
beforeEach(() => {
callApiSpy.mockImplementation(() => Promise.resolve());
});
afterEach(() => {
callApiSpy.mockClear();
});
describe("MyComponet", () => {
afterEach(() => {
jest.clearAllMocks();
});
it("should not call api when dontCallApi is set", async () => {
render(<MyComponent dontCallApi />);
await waitFor(() => expect(callApiSpy).toHaveBeenCalledTimes(0));
});
it("should call api when is not set", async () => {
render(<MyComponent />);
await waitFor(() => expect(callApiSpy).toHaveBeenCalledTimes(1));
});
});
to get more information about this take a look at the async utilities docs

Test intermediary state in async handler with React and Enzyme

Despite reading the documentation of enzyme, and act, I could not find a response to my use case because the examples only show simple use cases.
I have a React component displaying a button. The onClick handler sets a loading boolean and calls an external API. I want to assert that the component shows the loading indicator when we click on the button.
Here is the component:
export default function MyButton(): ReactElement {
const [loading, setLoading] = useState<boolean>(false);
const [data, setData] = useState<any>(null);
const onClick = async (): Promise<void> => {
setLoading(true);
const response = await fetch('/uri');
setData(await response.json());
setLoading(false);
};
if (loading) {
return <small>Loading...</small>;
}
return (
<div>
<button onClick={onClick}>Click Me!</button>
<div>
{data}
</div>
</div>
);
}
And here is the test:
test('should display Loading...', async () => {
window.fetch = () => Promise.resolve({
json: () => ({
item1: 'item1',
item2: 'item2',
}),
});
const component = mount(<MyButton />);
// Case 1 ✅ => validates the assertion BUT displays the following warning
component.find('button').simulate('click');
// Warning: An update to MyButton inside a test was not wrapped in act(...).
// When testing, code that causes React state updates should be wrapped into act(...):
// act(() => {
/* fire events that update state */
// });
/* assert on the output */
// This ensures that you're testing the behavior the user would see in the browser. Learn more at [URL to fb removed because SO does not accept it]
// Case 2 ❌ => fails the assertion AND displays the warning above
act(() => {
component.find('button').simulate('click');
});
// Case 3 ❌ => fails the assertion BUT does not display the warning
await act(async () => {
component.find('button').simulate('click');
});
expect(component.debug()).toContain('Loading...');
});
As you can see, if I get rid of the warning, my test is not satisfying anymore as it waits for the promise to resolve. How can we assert the intermediate state change while using act?
Thanks.
Just resolve promise manually:
const mockedData = {
json: () => ({
item1: 'item1',
item2: 'item2',
}),
};
let resolver;
window.fetch = () => new Promise((_resolver) => {
resolver = _resolver;
});
// ....
await act(async () => {
component.find('button').simulate('click');
});
expect(component.debug()).toContain('Loading...');
resolver(mockedData);
expect(component.debug()).not.toContain('Loading...');
PS but in sake of readability I'd rather have 2 separate tests: one with new Promise(); that never resolves and another with Promise.resolve(mockedData) that would be resolved automatically

Jest mock a fetch function from a connected component

I have some code which works. However for my test I would like to mock the fetch that is done in the component.
The test
I am trying the following:
import ConnectedComponent from './Component';
import { render } from '#testing-library/react';
import user from '../__models__/user'; // arbitrary file for the response
// create a mock response
const mockSuccessResponse = user;
const mockJsonPromise = Promise.resolve(mockSuccessResponse);
const mockFetchPromise = Promise.resolve({
json: () => mockJsonPromise,
});
// Trying mock the refetch from http
jest.mock('./http', () => {
return {
refetch: () => ({
settingsFetch: () => mockFetchPromise,
})
}
});
it('renders', async () => {
const { getByText } = render(Component);
const title = await getByText('My title');
expect(title).toBeInTheDocument();
});
Error message
With this I receive the following error:
● Test suite failed to run
TypeError: (0 , _http.refetch)(...) is not a function
The Application code
This code is working fine in my application. To give you an example:
./http.js
import { connect } from 'react-refetch';
export async function fetchWithToken(urlOrRequest, options = {}) {
// some stuff
return response;
}
export const refetch = connect.defaults({
fetch: fetchWithToken,
});
./Component.jsx
import { refetch } from './http';
const Component = ({ settingsFetch }) => <AnotherComponent settingsFetch={settingsFetch} />);
const ConnectedComponent = refetch(
({
match: { params: { someId } },
}) => ({
settingsFetch: {
url: 'http://some-url/api/v1/foo'
}
})
)(Component)
export default ConnectedComponent;
How can I mock this function to return a mocked Promise as the response?
Update: It's getting close by doing the following:
jest.mock('../helpers/http', () => ({
refetch: () => jest.fn(
(ReactComponent) => (ReactComponent),
),
}));
Now the error reads:
Warning: Failed prop type: The prop `settingsFetch` is marked as required in `ConnectedComponent`, but its value is `undefined`.
Which means I will probably have to provide the mocked responses for the fetches in there somewhere.
Jest itself is in charge of the modules. So in the following example you will see that the module coming from '../http' can be mocked.
You can then overwrite the props of that module by first adding the default props, and after that overwrite the ones you need with your own.
jest.mock('../http', () => {
return {
refetch: function(hocConf) {
return function(component) {
component.defaultProps = {
...component.defaultProps,
settingsFetch: {},
// remember to add a Promise instead of an empty object here
};
return component;
};
},
};
});

How do test async components with Jest?

can anyone tell me how to wait in jest for a mocked promise to resolve when mounting a component that calls componendDidMount()?
class Something extends React.Component {
state = {
res: null,
};
componentDidMount() {
API.get().then(res => this.setState({ res }));
}
render() {
if (!!this.state.res) return
return <span>user: ${this.state.res.user}</span>;
}
}
the API.get() is mocked in my jest test
data = [
'user': 1,
'name': 'bob'
];
function mockPromiseResolution(response) {
return new Promise((resolve, reject) => {
process.nextTick(
resolve(response)
);
});
}
const API = {
get: () => mockPromiseResolution(data),
};
Then my testing file:
import { API } from 'api';
import { API as mockAPI } from '__mocks/api';
API.get = jest.fn().mockImplementation(mockAPI.get);
describe('Something Component', () => {
it('renders after data loads', () => {
const wrapper = mount(<Something />);
expect(mountToJson(wrapper)).toMatchSnapshot();
// here is where I dont know how to wait to do the expect until the mock promise does its nextTick and resolves
});
});
The issue is that I the expect(mountToJson(wrapper)) is returning null because the mocked api call and lifecycle methods of <Something /> haven't gone through yet.
Jest has mocks to fake time travelling, to use it in your case, I guess you can change your code in the following style:
import { API } from 'api';
import { API as mockAPI } from '__mocks/api';
API.get = jest.fn().mockImplementation(mockAPI.get);
jest.useFakeTimers(); // this statement makes sure you use fake timers
describe('Something Component', () => {
it('renders after data loads', () => {
const wrapper = mount(<Something />);
// skip forward to a certain time
jest.runTimersToTime(1);
expect(mountToJson(wrapper)).toMatchSnapshot();
});
});
Alternatively to jest.runTimersToTime() you could also use jest.runAllTimers()
As a workaround convert it from async to sync
jest.spyOn(Api, 'get').mockReturnValue({
then: fn => fn('hello');
});

setState value used in componentDidMount is not reflected in Enzyme test

Component.js
import React from 'react'
import request from 'superagent'
export default React.createClass({
getInitialState() {
return {cats: []}
},
componentDidMount() {
request('/api', (err, res) => {
if (err) return;
this.setState({
cats: res.body.results
})
})
},
render() {
let cats = this.state.cats
let catsList = (
<ul>
{cats.map((c) => <li key={c.id}>cat</li>)}
</ul>
)
return (
<div>
{cats.length ? catsList : null}
</div>
)
}
})
Component.test.js
jest.unmock('../app.js')
jest.unmock('superagent')
import React from 'react'
import {mount} from 'enzyme'
import nock from 'nock'
import App from '../app.js'
describe('App', () => {
let ajaxFn
beforeEach(() => {
ajaxFn = nock('http://localhost')
.get('/api')
.reply(200, {
results: [{id: 1}, {id: 2}, {id: 3}]
})
})
it('renders correct number of cats', () => {
let wrapper = mount(<App />)
expect(wrapper.state('cats').length).toBe(3)
})
})
The test does not pass. wrapper.state('cats').length is always 0.
I understand that setState doesn't guarantee to update state immediately,
however if I log 'cats' in the component, I can see it updating.
If you end up setting state in your component in some context that enzyme doesn't know about, you will have to manually call .update() on the wrapper in order for it to get the updated version of the render tree.
it('renders correct number of cats', () => {
let wrapper = mount(<App />)
expect(wrapper.update().state('cats').length).toBe(3)
})
I had a similar problem and it was necessary to return a promise from the it callback and check the expectation in the then method of the promise.
In your case (assuming ajaxFn was a promise, or you could turn it into one) I think this would be approximately:
it('renders correct number of cats', () => {
let wrapper = mount(<App />)
return ajaxFn.then(() => {
expect(wrapper.state('cats').length).toBe(3);
}
})
I am not familiar with all the libraries you are using, but since your code is being executed asynchronously the test is finishing before the state can be updated. I was able to solve this problem by adding async/await to the test:
it('renders correct number of cats', async () => {
let wrapper = await mount(<App />)
expect(wrapper.state('cats').length).toBe(3)
})

Resources