How do I write a unit test in Jest for the initializePlayers function inside the useEffect?
Test if the call is working?
export default function App() {
...
useEffect(() => {
const initializePlayers = async () => {
await axios.get(url)
.then(async res=> {
const activePlayers = res.data.filter(p => p.active === true);
setPlayers(activePlayers);
}).catch(err => {
console.log(err);
})
}
initializePlayerPool();
}, []);
Please try this example.
import React from "react";
import { mount, shallow } from "enzyme";
import axios from "axios";
import { act } from "react-dom/test-utils";
import App from "./App";
jest.mock("axios");
// mock data
const url= "YOUR_URL",
describe("App test", () => {
let wrapper;
// clear all mocks
afterEach(() => {
jest.clearAllMocks();
});
test("load app", async () => {
// mock axios promise
await act(async () => {
await axios.get.mockImplementationOnce(() => Promise.resolve(url));
wrapper = mount(<App />);
});
wrapper.update();
await expect(axios.get).toHaveBeenCalledTimes(1);
});
});
Related
I'm practicing mocking with a simple React app using Jest + React Testing Library.
I did successfully mock fetch to test the app's functionality but I'm wondering why setting up the mock with beforeEach hook works while with the beforeAll hook my tests fail.
This is the (working) test code:
import {
render,
screen,
waitForElementToBeRemoved,
} from "#testing-library/react";
import App from "./App";
import { mockFetch } from "./mock-fetch";
import { mockResponse } from "./utils";
beforeEach(() => {
jest.spyOn(window, "fetch").mockImplementation(mockFetch);
});
describe("<App />", () => {
it("renders correctly a title", async () => {
render(<App />);
await waitForLoading();
expect(screen.getByText(/list of posts/i)).toBeInTheDocument();
});
describe("when requesting for posts", () => {
it("renders the list of posts once they are successfully fetched", async () => {
render(<App />);
await waitForLoading();
mockResponse.products.forEach((product) => {
expect(screen.getByText(product.title)).toBeInTheDocument();
expect(screen.getByText(product.price)).toBeInTheDocument();
});
});
it("should render an error message when posts fetch is not successful", async () => {
const error = "Error";
window.fetch.mockResolvedValueOnce({
ok: false,
status: 500,
});
render(<App />);
await waitForLoading();
expect(screen.getByText(error)).toBeInTheDocument();
});
it("should render a no posts message if there are no posts to show", async () => {
window.fetch.mockResolvedValueOnce({
ok: true,
status: 200,
json: () =>
Promise.resolve({
products: [],
}),
});
render(<App />);
await waitForLoading();
expect(screen.getByText(/no posts.../i)).toBeInTheDocument();
});
});
});
function waitForLoading() {
return waitForElementToBeRemoved(() => screen.queryByText(/loading.../i));
}
This is the App component:
import { useState, useEffect } from "react";
import Products from "./Products";
import "./App.css";
import { fetchProducts } from "./fetch-products";
function App() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
useEffect(() => {
fetchProducts()
.then((results) => {
setProducts(results.products);
})
.catch((err) => setError(err.message))
.finally(() => setLoading(false));
}, []);
if (error) return <div>{error}</div>;
return (
<div>
<h1>List of posts</h1>
{loading ? <p>Loading...</p> : <Products products={products} />}
</div>
);
}
export default App;
When using beforeAll instead of beforeEach the assertions fail because it can't find the elements I'm querying for. Also, I noticed that the fetchProducts function throws, this error is being set on the error state and in the test it's rendering the div containing the error with the message: Cannot read properties of undefined (reading 'ok').
Following are my code which includes the fetch API(getData) call with the useEffect and once get the response it will set the result into the setData using useState
I am trying to write the test case for the useEffect and useState but its failing and when I am seeing into the coverage ,I am getting the red background color with statements not covered for the useEffect block.
import { getData } from '../../api/data';
const [data, setData] = useState({});
useEffect(() => {
getData({ tableName }).then((response) => {
try {
if (response && response.result) {
const result = Array.isArray(response.result)
? response.result[0]
: response.result;
const createDate = result.createdDate;
result.name = result.firstName;
result.submittedDate = `${createDate}`;
result.attribute = Array.isArray(result.attribute)
? result.attribute
: JSON.parse(result.attribute);
setData(result);
}
} catch (error) {
const errorObj = { error: error.message || 'error' };
setData({ errorObj });
}
});
}, []);
And I tried to write the test cases as following for the above code.
import React from "react";
import {
shallowWithIntl,
loadTranslation,
} from "../../../node_modules/enzyme-react-intl/lib/enzyme-react-intl";
import ParentPage from "ParentPage";
import ChildPage from "ChildPage";
import mockResponse from "mockData";
import { shallow, mount } from "enzyme";
import { act } from "react-dom/test-utils";
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve(mockResponse),
})
);
describe("ParentPage", () => {
let useEffect;
let wrapper;
const mockUseEffect = () => {
useEffect.mockImplementationOnce((f) => f());
};
beforeEach(() => {
const defaultProps = {
tableName: "tableName",
};
wrapper = shallowWithIntl(<ParentPage {...defaultProps} />);
useEffect = jest.spyOn(React, "useEffect");
mockUseEffect();
});
it("Should render", () => {
expect(wrapper).toMatchSnapshot();
});
it("Compenent render", async () => {
let wrapper;
await act(async () => {
const setWidgets = jest.fn();
const useStateSpy = jest.spyOn(React, "useState");
useStateSpy.mockImplementation([mockResponse, setWidgets]);
wrapper = await mount(<ChildPage data={mockResponse} />);
await act(async () => {
wrapper.update();
});
console.log(wrapper);
});
});
});
But when I tried using npm run test,And check the coverage I am still getting the statements not covered for the useEffect and useState.
What should I do to achieve the coverage as maximum as possible?
I have written this component. it fetchs data using hooks and state. Once it is fetched the loading state is changed to false and show the sidebar.
I faced a problem with Jest and Enzyme, as it does throw a warning for Act in my unit test. once I add the act to my jest and enzyme the test is failed!
// #flow
import React, { useEffect, useState } from 'react';
import Sidebar from '../components/Sidebar';
import fetchData from '../apiWrappers/fetchData';
const App = () => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const getData = async () => {
try {
const newData = await fetchData();
setData(newData);
setLoading(false);
}
catch (e) {
setLoading(false);
}
};
getData();
// eslint-disable-next-line
}, []);
return (
<>
{!loading
? <Sidebar />
: <span>Loading List</span>}
</>
);
};
export default App;
And, I have added a test like this which works perfectly.
import React from 'react';
import { mount } from 'enzyme';
import fetchData from '../apiWrappers/fetchData';
import data from '../data/data.json';
import App from './App';
jest.mock('../apiWrappers/fetchData');
const getData = Promise.resolve(data);
fetchData.mockReturnValue(getData);
describe('<App/> Rendering using enzyme', () => {
beforeEach(() => {
fetchData.mockClear();
});
test('After loading', async () => {
const wrapper = mount(<App />);
expect(wrapper.find('span').at(0).text()).toEqual('Loading List');
const d = await fetchData();
expect(d).toHaveLength(data.length);
wrapper.update();
expect(wrapper.find('span').exists()).toEqual(false);
expect(wrapper.html()).toMatchSnapshot();
});
});
So, I got a warning:
Warning: An update to App 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 */
});
I did resolve the warning like this using { act } react-dom/test-utils.
import React from 'react';
import { act } from 'react-dom/test-utils';
import { mount } from 'enzyme';
import fetchData from '../apiWrappers/fetchData';
import data from '../data/data.json';
import App from './App';
jest.mock('../apiWrappers/fetchData');
const getData = Promise.resolve(data);
fetchData.mockReturnValue(getData);
describe('<App/> Rendering using enzyme', () => {
beforeEach(() => {
fetchData.mockClear();
});
test('After loading', async () => {
await act(async () => {
const wrapper = mount(<App />);
expect(wrapper.find('span').at(0).text()).toEqual('Loading List');
const d = await fetchData();
expect(d).toHaveLength(data.length);
wrapper.update();
expect(wrapper.find('span').exists()).toEqual(false);
expect(wrapper.html()).toMatchSnapshot();
});
});
});
But, then my test is failed.
<App/> Rendering using enzyme › After loading
expect(received).toEqual(expected) // deep equality
Expected: false
Received: true
35 |
36 | wrapper.update();
> 37 | expect(wrapper.find('span').exists()).toEqual(false);
Does anybody know why it fails? Thanks!
"react": "16.13.1",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.3",
This issue is not new at all. You can read the full discussion here: https://github.com/enzymejs/enzyme/issues/2073.
To sum up, currently in order to fix act warning, you have to wait a bit before update your wrapper as following:
const waitForComponentToPaint = async (wrapper) => {
await act(async () => {
await new Promise(resolve => setTimeout(resolve));
wrapper.update();
});
};
test('After loading', async () => {
const wrapper = mount(<App />);
expect(wrapper.find('span').at(0).text()).toEqual('Loading List');
// before the state updated
await waitForComponentToPaint(wrapper);
// after the state updated
expect(wrapper.find('span').exists()).toEqual(false);
expect(wrapper.html()).toMatchSnapshot();
});
You should not wrap your whole test in act, just the part that will cause state of your component to update.
Something like the below should solve your problem.
test('After loading', async () => {
await act(async () => {
const wrapper = mount(<App />);
});
expect(wrapper.find('span').at(0).text()).toEqual('Loading List');
const d = await fetchData();
expect(d).toHaveLength(data.length);
await act(async () => {
wrapper.update();
})
expect(wrapper.find('span').exists()).toEqual(false);
expect(wrapper.html()).toMatchSnapshot();
});
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
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));
});
})