I have a React hooks functional component that I'd like to test with Jest/Enzyme. I would like test its tertiary render behaviour based upon a useState value. I can't seem to find any example online. There is no 'click' to simulate - no API call to mock because at the end, I still need to test based upon the useState value. In the past, with class components, I could set the state. With the new hooks, I can't. So, basically - how do I mock an async await inside a mocked submitForm function so that the render behaves properly?
Here's my component:
import React, { useState } from 'react';
import { Redirect } from 'react-router-dom';
import Form from 'core/Form';
export const Parent = ({submitForm}) => {
const [formValues, setFormValues] = useState({});
const [redirect, setRedirect] = useState(false);
const handleChange = name => evt => {
setFormValues({ ...formValues, [name]: evt.target.value });
};
const onSubmit = async () => {
try {
const res = await submitForm(formValues);
if (res) setRedirect(true);
else setRedirect(false);
} catch (err) {
console.log('Submit error: ', err);
}
};
return redirect ? (
<Redirect push to={path} />
) : (
<Form onSubmit={onSubmit} values={formValues} onChange={handleChange} />
);
};
export default Parent;
Here's my testing so far:
import React from 'react';
import { shallow } from 'enzyme';
import { Redirect } from 'react-router-dom';
import Parent from './Parent';
import Form from 'core/Form';
let wrapper, props;
.
.
.
describe('<Parent /> rendering', () => {
beforeEach(() => {
props = createTestProps();
wrapper = shallow(<Parent {...props} />);
});
afterEach(() => {
jest.clearAllMocks();
});
const setState = jest.fn();
const useStateSpy = jest.spyOn(React, 'useState');
useStateSpy.mockImplementation(init => [init, setState]);
it('Should render 1 Form', () => {
expect(wrapper.find(Form)).toHaveLength(1);
});
it('renders Redirect after API call', () => {
setRedirect = jest.fn(() => false);
expect(wrapper.find(Redirect)).toHaveLength(1);
});
it('renders Form before API call', () => {
setRedirect = jest.fn(() => true);
expect(wrapper.find(Form)).toHaveLength(1);
});
});
Testing the useState Hook with Enzyme js file with the following: import React from "react"; const App= () => { const [name, setName] = React. useState(""); return ( <form> <div className="row"> <div className="col-md-6"> <input type="text" placeholder="Enter your name" className="input" onChange={(e) => { setName(e.
If you need to test a custom Hook, you can do so by creating a component in your test, and using your Hook from it. Then you can test the component you wrote. To reduce the boilerplate, we recommend using React Testing Library which is designed to encourage writing tests that use your components as the end users do.
TL;DR: useState is an asynchronous hook and it doesn't change the state immediately, it has to wait for the component to re-render. useRef is a synchronous hook that updates the state immediately and persists its value through the component's lifecycle, but it doesn't trigger a re-render.
To update the state, call the state updater function with the new state setState(newState) . Alternatively, if you need to update the state based on the previous state, supply a callback function setState(prevState => newState) .
You don't need to spy useState
hook. Which means you should not test these hooks and methods of the component directly. Instead, you should test components' behavior(the state
, props
and what is rendered)
E.g.
index.tsx
:
import React, { useState } from 'react';
import { Redirect } from 'react-router-dom';
export const Form = ({ onSubmit, onChange, values }) => <form onSubmit={onSubmit}></form>;
const path = '/user';
export const Parent = ({ submitForm }) => {
const [formValues, setFormValues] = useState({});
const [redirect, setRedirect] = useState(false);
const handleChange = (name) => (evt) => {
setFormValues({ ...formValues, [name]: evt.target.value });
};
const onSubmit = async () => {
try {
const res = await submitForm(formValues);
if (res) setRedirect(true);
else setRedirect(false);
} catch (err) {
console.log('Submit error: ', err);
}
};
return redirect ? (
<Redirect push to={path} />
) : (
<Form onSubmit={onSubmit} values={formValues} onChange={handleChange} />
);
};
export default Parent;
index.test.tsx
:
import Parent, { Form } from './';
import React from 'react';
import { shallow } from 'enzyme';
import { Redirect } from 'react-router-dom';
import { act } from 'react-dom/test-utils';
const whenStable = async () =>
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});
describe('60137762', () => {
it('should render Form', () => {
const props = { submitForm: jest.fn() };
const wrapper = shallow(<Parent {...props}></Parent>);
expect(wrapper.find(Form)).toBeTruthy();
});
it('should handle submit and render Redirect', async () => {
const props = { submitForm: jest.fn().mockResolvedValueOnce(true) };
const wrapper = shallow(<Parent {...props}></Parent>);
wrapper.find(Form).simulate('submit');
await whenStable();
expect(props.submitForm).toBeCalledWith({});
expect(wrapper.find(Redirect)).toBeTruthy();
});
it('should handle submit and render Form', async () => {
const props = { submitForm: jest.fn().mockResolvedValueOnce(false) };
const wrapper = shallow(<Parent {...props}></Parent>);
wrapper.find(Form).simulate('submit');
await whenStable();
expect(props.submitForm).toBeCalledWith({});
expect(wrapper.find(Form)).toBeTruthy();
});
it('should handle error if submit failure', async () => {
const logSpy = jest.spyOn(console, 'log');
const mError = new Error('network');
const props = { submitForm: jest.fn().mockRejectedValueOnce(mError) };
const wrapper = shallow(<Parent {...props}></Parent>);
wrapper.find(Form).simulate('submit');
await whenStable();
expect(props.submitForm).toBeCalledWith({});
expect(logSpy).toHaveBeenCalledWith('Submit error: ', mError);
});
});
Unit test results with coverage report:
PASS stackoverflow/60137762/index.test.tsx
60137762
✓ should render Form (18ms)
✓ should handle submit and render Redirect (15ms)
✓ should handle submit and render Form (8ms)
✓ should handle error if submit failure (18ms)
console.log node_modules/jest-environment-enzyme/node_modules/jest-mock/build/index.js:866
Submit error: Error: network
at /Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/stackoverflow/60137762/index.test.tsx:39:20
at step (/Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/stackoverflow/60137762/index.test.tsx:44:23)
at Object.next (/Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/stackoverflow/60137762/index.test.tsx:25:53)
at /Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/stackoverflow/60137762/index.test.tsx:19:71
at new Promise (<anonymous>)
at Object.<anonymous>.__awaiter (/Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/stackoverflow/60137762/index.test.tsx:15:12)
at Object.<anonymous> (/Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/stackoverflow/60137762/index.test.tsx:37:47)
at Object.asyncJestTest (/Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/node_modules/jest-jasmine2/build/jasmineAsyncInstall.js:100:37)
at resolve (/Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/node_modules/jest-jasmine2/build/queueRunner.js:43:12)
at new Promise (<anonymous>)
at mapper (/Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/node_modules/jest-jasmine2/build/queueRunner.js:26:19)
at promise.then (/Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/node_modules/jest-jasmine2/build/queueRunner.js:73:41)
-----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------|---------|----------|---------|---------|-------------------
All files | 78.57 | 100 | 40 | 93.75 |
index.tsx | 78.57 | 100 | 40 | 93.75 | 12
-----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 4 passed, 4 total
Snapshots: 0 total
Time: 3.716s, estimated 5s
Source code: https://github.com/mrdulin/react-apollo-graphql-starter-kit/tree/master/stackoverflow/60137762
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With