To start I'm conditionally rendering a component that's reliant on the useState hook being set as a result of useEffect hook. Here's a code sample:
function Component() {
const [response, setResponse] = useState();
const [fail, setFail] = useState();
/**
* #function getModels - fetch data to populate the models
* */
const fetchStuff = async () => {
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then((data) => {
const storage = [data];
setResponse(storage);
setFail(false);
})
.catch((err) => {
setResponse(err);
setFail(true);
});
};
useEffect(() => {
fetchStuff();
}, []);
if (fail === true) {
return (
<p>
ERROR:
{fail}
</p>
);
}
if (fail === false) {
return (
<p>
Success:
{response}
</p>
);
}
return <p>Loading Screen</p>;
}
My current point of contention is that I'm unable to call setResponse or setFail and update the state of fail or response. I believe that I need to use mount as opposed to shallow rendering? Also, I understand that testing philosophies would argue against conducting a test in this fashion. However, I am seeking an a solution that enables updating the state. Any advice would be greatly appreciated.
You can mock fetch API call to and its resolved value. Then, you can assert what exactly the component renders. We should use whenStable function to make sure the mocked API calls are completed.
Component.jsx:
import React, { useState, useEffect } from 'react';
export function Component() {
const [response, setResponse] = useState();
const [fail, setFail] = useState();
/**
* #function getModels - fetch data to populate the models
* */
const fetchStuff = async () => {
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then((data) => {
const storage = [data];
setResponse(storage);
setFail(false);
})
.catch((err) => {
setResponse(err);
setFail(true);
});
};
useEffect(() => {
fetchStuff();
}, []);
if (fail === true) {
return (
<p>
ERROR:
{fail}
</p>
);
}
if (fail === false) {
return (
<p>
Success:
{response}
</p>
);
}
return <p>Loading Screen</p>;
}
Component.test.jsx:
import React from 'react';
import { Component } from './Component';
import { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
const whenStable = async () => {
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});
};
describe('65243384', () => {
let fetch;
beforeEach(() => {
fetch = global.fetch;
});
afterEach(() => {
global.fetch = fetch;
});
it('should success', async () => {
global.fetch = jest.fn().mockResolvedValueOnce('mocked success data');
const wrapper = mount(<Component></Component>);
expect(wrapper.find('p').text()).toBe('Loading Screen');
await whenStable();
expect(wrapper.find('p').text()).toBe('Success:mocked success data');
expect(global.fetch).toBeCalledWith('https://jsonplaceholder.typicode.com/todos/1');
});
it('should fail', async () => {
const mErr = new Error('network');
global.fetch = jest.fn().mockRejectedValueOnce(mErr);
const wrapper = mount(<Component></Component>);
expect(wrapper.find('p').text()).toBe('Loading Screen');
await whenStable();
expect(wrapper.find('p').text()).toBe('ERROR:');
expect(global.fetch).toBeCalledWith('https://jsonplaceholder.typicode.com/todos/1');
});
});
unit test result:
PASS examples/65243384/Component.test.jsx
65243384
✓ should success (44 ms)
✓ should fail (5 ms)
---------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
---------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
Component.jsx | 100 | 100 | 100 | 100 |
---------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 5.121 s
source code: https://github.com/mrdulin/jest-v26-codelab/tree/main/examples/65243384
Related
I'm trying to test a custom hook which setting a state in a Promise then/catch block.
This is my custom hook:
import {useState} from 'react';
import {useBackendApi} from './backend-api';
export function useMyCustomHook(): {status?: Status; loadData: () => void} {
const [status, setStatus] = useState<Status>();
const backendApi = useBackendApi();
const loadData = (): void => {
backendApi.makeApiCall().then(() => {
setStatus(Status.SUCCESS);
}).catch(() => {
setStatus(Status.ERROR);
});
};
return {status, loadData};
}
export enum Status {
SUCCESS = 1,
ERROR = 2,
}
And these are my tests:
import {act, renderHook, waitFor} from '#testing-library/react';
import * as BackendApiHooks from './backend-api';
import {Status, useMyCustomHook} from './my-custom-hook';
describe('useMyCustomHook', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('should set the status to SUCCESS when makeApiCall resolved', async () => {
jest.spyOn(BackendApiHooks, 'useBackendApi').mockReturnValue({
makeApiCall: jest.fn().mockReturnValue(Promise.resolve()),
});
const {result} = renderHook(() => useMyCustomHook());
await act(() => {
result.current.loadData();
});
await waitFor(() => expect(result.current.status).toBe(Status.SUCCESS));
});
it('should set the status to ERROR when makeApiCall rejected', async () => {
jest.spyOn(BackendApiHooks, 'useBackendApi').mockReturnValue({
makeApiCall: jest.fn().mockReturnValue(Promise.reject()),
});
const {result} = renderHook(() => useMyCustomHook());
await act(() => {
result.current.loadData();
});
await waitFor(() => expect(result.current.status).toBe(Status.ERROR));
});
});
My tests are passed but on the first test there is an error printed to the console:
Warning: An update to TestComponent 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 https://reactjs.org/link/wrap-tests-with-act
at TestComponent (/****/node_modules/#testing-library/react/dist/pure.js:281:5)
8 | const loadData = (): void => {
9 | backendApi.makeApiCall().then(() => {
> 10 | setStatus(Status.SUCCESS);
| ^
11 | }).catch(() => {
12 | setStatus(Status.ERROR);
13 | });
at printWarning (node_modules/react-dom/cjs/react-dom.development.js:86:30)
at error (node_modules/react-dom/cjs/react-dom.development.js:60:7)
at warnIfUpdatesNotWrappedWithActDEV (node_modules/react-dom/cjs/react-dom.development.js:27589:9)
at scheduleUpdateOnFiber (node_modules/react-dom/cjs/react-dom.development.js:25508:5)
at dispatchSetState (node_modules/react-dom/cjs/react-dom.development.js:17527:7)
at src/modules/table/components/my-custom-hook.ts:10:13
It happens although I wrapped the result.current.loadData() in act.
Important note: It happens only on the first test (with the resolve mock) but not on the second test (with the reject mock)
Couldn't find any relevant solution in other questions. What am I missing?
I am trying to test a simple component.
In my component, I am calling the fetchPdf function with useEffect.
Inside fetchPdf function I am making axios request and if the call was successful I am setting the response (which is the pdf) in the state.
My component:
import React, { useState, useEffect } from "react";
import axios from 'axios';
export default function App() {
const [pdf, setPdf] = useState();
const fetchPdf = () => {
// Here I am make an API call - on success I am
// set the pdf from the response into state
// axios.get('url/endpoint')
// .then((res) => {
// if (res.status === 200) {
// setPdf(res.data);
// }
// }).catch((e) => console.log(e.message));
setPdf("Mocked PDF");
};
useEffect(() => {
fetchPdf();
}, []);
return (
<div className="App">
<h1>My pdf from state: {pdf}</h1>
</div>
);
}
My test:
import React from "react";
import Enzyme from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import App from "./App";
Enzyme.configure({ adapter: new Adapter() });
describe("<App />", () => {
let wrapper;
const setState = jest.fn();
const useStateSpy = jest.spyOn(React, "useState");
useStateSpy.mockImplementation((init) => [init, setState]);
beforeEach(() => {
wrapper = Enzyme.shallow(<App />);
});
afterEach(() => {
jest.clearAllMocks();
});
describe("calling designPDF", () => {
it("set designPDF into the state", () => {
// Not sure how can I test the fetchPDF function as been called
});
});
});
Codesendbox example
useEffect does not support shallow rendering yet. So you should use Full Rendering API (mount(...)).
Besides, you should mock axios.get method and its resolved value. Call the whenStable function to ensure that the promise returned by the axios.get method is resolved or rejected
E.g.
App.jsx:
import React, { useState, useEffect } from 'react';
import axios from 'axios';
export default function App() {
const [pdf, setPdf] = useState();
const fetchPdf = () => {
axios
.get('url/endpoint')
.then((res) => {
if (res.status === 200) {
setPdf(res.data);
}
})
.catch((e) => console.log(e.message));
};
useEffect(() => {
fetchPdf();
}, []);
return (
<div className="App">
<h1>My pdf from state: {pdf}</h1>
</div>
);
}
App.test.jsx:
import App from './App';
import axios from 'axios';
import { mount } from 'enzyme';
import React from 'react';
import { act } from 'react-dom/test-utils';
const whenStable = async () => {
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});
};
describe('65310275', () => {
it('should get pdf', async () => {
const getSpy = jest.spyOn(axios, 'get').mockResolvedValueOnce({ data: 'Mocked PDF', status: 200 });
const wrapper = mount(<App></App>);
await whenStable();
expect(wrapper.find('h1').text()).toEqual('My pdf from state: Mocked PDF');
expect(getSpy).toBeCalledWith('url/endpoint');
getSpy.mockRestore();
});
it('should handle error', async () => {
const mErr = new Error('timeout');
const getSpy = jest.spyOn(axios, 'get').mockRejectedValueOnce(mErr);
const logSpy = jest.spyOn(console, 'log');
const wrapper = mount(<App></App>);
await whenStable();
expect(wrapper.find('h1').text()).toEqual('My pdf from state: ');
expect(getSpy).toBeCalledWith('url/endpoint');
expect(logSpy).toBeCalledWith('timeout');
getSpy.mockRestore();
});
});
unit test result:
PASS examples/65310275/App.test.jsx
65310275
✓ should get pdf (40 ms)
✓ should handle error (13 ms)
console.log
timeout
at CustomConsole.<anonymous> (node_modules/jest-environment-enzyme/node_modules/jest-mock/build/index.js:866:25)
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 100 | 50 | 100 | 100 |
App.jsx | 100 | 50 | 100 | 100 | 11
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 6.095 s
source code: https://github.com/mrdulin/jest-v26-codelab/tree/main/examples/65310275
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);
});
});
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
I want to test api call and data returned which should be displayed inside my functional component. I created List component which performs api call. I would like the returned data to be displayed in the component and I use the useState hook for this. Component looks like this:
const List: FC<{}> = () => {
const [data, setData] = useState<number>();
const getData = (): Promise<any> => {
return fetch('https://jsonplaceholder.typicode.com/todos/1');
};
React.useEffect(() => {
const func = async () => {
const data = await getData();
const value = await data.json();
setData(value.title);
}
func();
}, [])
return (
<div>
<div id="test">{data}</div>
</div>
)
}
I wrote one test in which I mocked the fetch method. I check if the fetch method has been called and it actually happens. Unfortunately, I don't know how I could test the value returned from response. When I try console.log I just get null and I'd like to get 'example text'. My guess is that I have to wait for this value returned from Promise. Unfortunately, despite trying with methods act and wait, I don't know how to achieve it. Here is my test:
it('test', async () => {
let component;
const fakeResponse = 'example text';
const mockFetch = Promise.resolve({json: () => Promise.resolve(fakeResponse)});
const mockedFetch = jest.spyOn(window, 'fetch').mockImplementationOnce(() => mockFetch as any )
await wait( async () => {
component = render(<List />);
})
const value: Element = component.container.querySelector('#test');
console.log(value.textContent);
expect(mockedFetch).toHaveBeenCalledTimes(1);
})
I would be really thankful for any suggestions.
Second Attempt
Also tried using data-testid="test" and waitForElement, but still receiving null value.
updated component deltas:
const List: FC<{}> = () => {
- const [data, setData] = useState<number>();
+ const [data, setData] = useState<string>('test');
const getData = (): Promise<any> => {
return fetch('https://jsonplaceholder.typicode.com/todos/1');
};
React.useEffect(() => {
const func = async () => {
const data = await getData();
const value = await data.json();
setData(value.title);
}
func();
}, [])
return (
<div>
- <div id="test">{data}</div>
+ <div data-testid="test" id="test">{data}</div>
</div>
)
}
and updated test:
it('test', async () => {
const fakeResponse = 'example text';
const mockFetch = Promise.resolve({json: () => Promise.resolve(fakeResponse)});
const mockedFetch = jest.spyOn(window, 'fetch').mockImplementationOnce(() => mockFetch as any )
const { getByTestId } = render(<List />);
expect(getByTestId("test")).toHaveTextContent("test");
const resolvedValue = await waitForElement(() => getByTestId('test'));
expect(resolvedValue).toHaveTextContent("example text");
expect(mockedFetch).toHaveBeenCalledTimes(1);
})
Here is a working unit testing example:
index.tsx:
import React, { useState, FC } from 'react';
export const List: FC<{}> = () => {
const [data, setData] = useState<number>();
const getData = (): Promise<any> => {
return fetch('https://jsonplaceholder.typicode.com/todos/1');
};
React.useEffect(() => {
const func = async () => {
const data = await getData();
const value = await data.json();
setData(value.title);
};
func();
}, []);
return (
<div>
<div data-testid="test">{data}</div>
</div>
);
};
index.test.tsx:
import { List } from './';
import React from 'react';
import '#testing-library/jest-dom/extend-expect';
import { render, waitForElement } from '#testing-library/react';
describe('59892259', () => {
let originFetch;
beforeEach(() => {
originFetch = (global as any).fetch;
});
afterEach(() => {
(global as any).fetch = originFetch;
});
it('should pass', async () => {
const fakeResponse = { title: 'example text' };
const mRes = { json: jest.fn().mockResolvedValueOnce(fakeResponse) };
const mockedFetch = jest.fn().mockResolvedValueOnce(mRes as any);
(global as any).fetch = mockedFetch;
const { getByTestId } = render(<List></List>);
const div = await waitForElement(() => getByTestId('test'));
expect(div).toHaveTextContent('example text');
expect(mockedFetch).toBeCalledTimes(1);
expect(mRes.json).toBeCalledTimes(1);
});
});
unit test result:
PASS src/stackoverflow/59892259/index.test.tsx (9.816s)
59892259
✓ should pass (63ms)
-----------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
-----------|----------|----------|----------|----------|-------------------|
All files | 100 | 100 | 100 | 100 | |
index.tsx | 100 | 100 | 100 | 100 | |
-----------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 11.73s, estimated 13s
waitForElement is not available from '#testing-library/react' anymore. Docs
Another approach is:
import { act, render } from '#testing-library/react';
it('is a test definition', async () => { // notice the async
await act(async () => { // this is kind of ugly, but it works.
render(<TheComponent />
})
// this section will run after the effects within TheComponent were triggered
})
What worked for me was a combination of both answers
it("should fetch data", async ()=>{
const fakeResponse = {title : "Test"}
const mRes = { json: jest.fn().mockResolvedValueOnce(fakeResponse) };
const mockedFetch = jest.fn().mockResolvedValueOnce(mRes);
global.fetch = mockedFetch;
render(<Component/>);
await act(async ()=>{
await waitFor(() => expect(mockedFetch).toHaveBeenCalledTimes(1))
})
})
NB: the code above is in javascript, but I don't think there is much difference between js and ts
I have a simple component, it fetches async list of posts.
export const Posts = () => {
const [list, dispatch] = useReducer(listReducer, []);
useEffect(() => {
fetchList(dispatch);
}, []);
return (
<ul>
{list.map((el) => (
<li key={el.id}>{el.title}</li>
))}
</ul>
);
};
In other file i keep the logic:
export const fetchList = async (dispatch) => {
try {
const result = await api.get('/list/') /* AXIOS */
dispatch({ type: LIST_SUCCES, payload: result.data.list })
} catch (error) {
dispatch({ type: LIST_FAILURE })
}
}
export const listReducer = (state, action) => {
switch (action.type) {
case LIST_SUCCES:
return action.payload
case LIST_FAILURE:
return []
default:
throw new Error()
}
}
I've tried multiple libraries but i'm just unable to write a test. How can i write Posts.test.js to check if post are fetched and displayed, i'm triggering async fetchList after first mount of the component (so it is not componentDidMount), and after data are fetched i dispatch action from that async function and update the list.
Here is the unit test solution:
index.tsx:
import React, { useReducer, useEffect } from 'react';
import { listReducer, fetchList } from './reducer';
export const Posts = () => {
const [list, dispatch] = useReducer(listReducer, []);
useEffect(() => {
fetchList(dispatch);
}, []);
return (
<ul>
{list.map((el) => (
<li key={el.id}>{el.title}</li>
))}
</ul>
);
};
reducer.ts:
import axios from 'axios';
const LIST_SUCCES = 'LIST_SUCCES';
const LIST_FAILURE = 'LIST_FAILURE';
export const fetchList = async (dispatch) => {
try {
const result = await axios.get('/list/'); /* AXIOS */
dispatch({ type: LIST_SUCCES, payload: result.data.list });
} catch (error) {
dispatch({ type: LIST_FAILURE });
}
};
export const listReducer = (state, action) => {
switch (action.type) {
case LIST_SUCCES:
return action.payload;
case LIST_FAILURE:
return [];
default:
throw new Error();
}
};
index.spec.tsx:
import React from 'react';
import { Posts } from './';
import { mount } from 'enzyme';
import axios from 'axios';
import { act } from 'react-dom/test-utils';
describe('Posts', () => {
afterAll(() => {
jest.restoreAllMocks();
});
it('should render list correctly', async () => {
const mResponse = { data: { list: [{ id: 1, title: 'jest' }] } };
jest.spyOn(axios, 'get').mockResolvedValueOnce(mResponse);
const wrapper = mount(<Posts></Posts>);
expect(wrapper.find('ul').children()).toHaveLength(0);
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});
wrapper.update();
expect(wrapper.find('ul').children()).toHaveLength(1);
expect(wrapper).toMatchInlineSnapshot(`
<Component>
<ul>
<li
key="1"
>
jest
</li>
</ul>
</Component>
`);
});
it('should render empty list when request list data failed', async () => {
const mError = new Error('Internal server error');
jest.spyOn(axios, 'get').mockRejectedValueOnce(mError);
const wrapper = mount(<Posts></Posts>);
expect(wrapper.find('ul').children()).toHaveLength(0);
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});
wrapper.update();
expect(wrapper.find('ul').children()).toHaveLength(0);
expect(wrapper).toMatchInlineSnapshot(`
<Component>
<ul />
</Component>
`);
});
});
Unit test result with coverage report:
PASS src/stackoverflow/59197574/index.spec.tsx (12.494s)
Posts
✓ should render list correctly (105ms)
✓ should render empty list when request list data failed (37ms)
› 1 snapshot written.
------------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
------------|----------|----------|----------|----------|-------------------|
All files | 95.83 | 66.67 | 100 | 95 | |
index.tsx | 100 | 100 | 100 | 100 | |
reducer.ts | 92.86 | 66.67 | 100 | 91.67 | 21 |
------------|----------|----------|----------|----------|-------------------|
Snapshot Summary
› 1 snapshot written from 1 test suite.
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 1 written, 1 passed, 2 total
Time: 14.409s
Source code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/59197574