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
Related
I am trying to test rendered data on my page that is coming from an api. The api generates one of three random objects, in this case they are weather and contain the following types:
forcast: string;
max: number;
min: number;
description: string;
I'm new to testing and typescript and am wondering how I can find say the forecast on a page if it is going to be randomised each call.
here is my code thus far:
Weather.tsx
import axios from 'axios';
import { useEffect, useState } from 'react';
import { IWeather } from '../interfaces/IWeather';
const Weather = () => {
const [minTemp, setMinTemp] = useState<IWeather[]>([]);
const [maxTemp, setMaxTemp] = useState<IWeather[]>([]);
const [forcast, setForcast] = useState([]);
const fetchWeatherData = async () => {
const response = await axios.get('http://mock-api-call/weather/get-weather');
setMinTemp(response.data.result.weather.min);
setMaxTemp(response.data.result.weather.max);
setForcast(response.data.result.weather.forcast);
};
useEffect(() => {
fetchWeatherData();
}, []);
return (
<div className="container">
<p className="forcast">{forcast}</p>
<p className="temperature">Temperature {`${minTemp} to ${maxTemp}`}</p>
</div>
);
};
export default Weather;
And this is a basic version of say testing for all the forecast elements. I have tried adding || and searching for multiple strings but unfortunately is not working
Weather.test.tsx
import { render, screen } from '#testing-library/react';
import Weather from '../Weather';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
const server = setupServer(
rest.get('http://mock-api-call/weather/get-weather', (req, res, ctx) => {
return res(ctx.json({}));
}),
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
describe('Weather', () => {
test('should render forcast for day', async () => {
render(<Weather />);
const forecast = await screen.findAllByText(/sunny/i || /overcast/i || /snowing/i);
expect(forecast).toBeInTheDocument();
});
});
I am also receiving this TypeError
TypeError: Cannot read properties of undefined (reading 'weather')
10 | const fetchWeatherData = async () => {
11 | const response = await axios.get('http://mock-api-call/weather/get-weather');
> 12 | setMinTemp(response.data.result.weather.min);
| ^
13 | setMaxTemp(response.data.result.weather.max);
14 | setForcast(response.data.result.weather.forcast);
15 | };
For clarity the data is rendering onto the page.
I am trying to test a functional component which is below :
export const MyFunctionalComponent=()=>{
const {id, otherid} = useSelector((state) => state.user);
const accounts = useSelector((state) => state.Selection.accounts);
const dispatch = useDispatch();
useEffect(() => {
if (!accounts) {
dispatch(getDetails(id, otherid));
}
}, []);
const handleOnClick = (Id) => {
dispatch(saveId(Id));
};
if (!accounts) {
return <CircularLoader />;
}
if (!accounts.length) {
return (
<AccessUnavailablePanel
/>
);
}
if (accounts.length === 1) {
return <Redirect to={`${accounts[0].id}`} />;
}
return (
<MyList
accounts={accounts}
handleOnClick={handleOnClick}
/>
);
};
i am new to JEST , how can i correctly mock useSelector, useDispatch and useEffect ?
i am trying to test as follows :
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useSelector: jest.fn(),
useDispatch: jest.fn()
}));
describe('<MyFunctionalComponent/>', () => {
const useEffect=jest.spyOn(React, "useEffect").mockImplementation(() => {})
const useSelectorMock = reactRedux.useSelector;
const useDispatchMock = reactRedux.useDispatch;
const user = {
id: '32sdfsdfr-fjdfdk33-4434sdfs',
otherid: '73587hdfjg-dfghd94-jgdj'
};
const store = mockStore({
user:user,
Selection: {
Id: '',
accounts: null
}
});
let wrapper;
const setup = () => {
const {result} = renderHook(() => MyFunctionalComponent(), {
wrapper: ({children}) => (
<Provider store={store}>{children}</Provider>
)
});
return result;
};
describe('Rendering accounts available', () => {
beforeEach(() => {
useDispatchMock.mockImplementation(() => () => {});
useSelectorMock.mockImplementation((selector) =>
selector(mockStore)
);
});
afterEach(() => {
jest.clearAllMocks();
useSelectorMock.mockClear();
useDispatchMock.mockClear();
});
it('should render the accounts if more than 1 account available', () => {
useSelectorMock.mockReturnValue(user);
const mockdata = {
accounts: [
{
id: '637ghgh',
},
{
id: '10a190abd',
}
]
};
const getDetails = jest.spyOn(
SelectionActions,
'getDetails'
);
useSelectorMock.mockReturnValue(mocdata);
useDispatchMock.mockReturnValue(
getDetails(user.id, user.otherid)
);
wrapper = setup();
store.dispatch(getDetails(user.id, user.otherid))
sleep(500);
const actions=store.getActions();
console.debug(actions[0])
expect(getDetails).toHaveBeenCalledWith(user.id, user.otherid);
expect(actions[0].type).toEqual('GET_DETAILS')
expect(wrapper.current).toMatchSnapshot();
});
});
But my snapshot seems to return AccessUnavialable, and the payload is undefined. How can I modify my tests to correctly return the mocked data as payload and get the correct snapshot?
It can be tested by mocking libraries such as react-redux, but it is not recommended. It pays too much attention to implementation details. A better way is to use the mock store to provide only mock data instead of mock implementation details. Use the original, unmocked useSelector and useEffect hooks, which are closer to the real function of the code, rather than the implementation details of the mock.
In addition, creating mock objects and their implementation are also very cumbersome steps. You should also pay attention to cleaning up mock objects to avoid being used by other test cases, resulting in test failures.
Use redux-mock-store to create a mock store with the mocked state.
Each test case provides different mock data to drive component rendering, and finally, assert whether the component rendering is correct.
For simplicity, just to demonstrate this test method, I replaced your component with a simple component, the principle is the same.
E.g.
index.tsx:
import { useSelector, useDispatch } from 'react-redux';
import { useEffect } from 'react';
import React from 'react';
const getDetails = (id, otherId) => ({ type: 'GET_DETAILS' });
export const MyFunctionalComponent = () => {
const { id, otherid } = useSelector((state: any) => state.user);
const accounts = useSelector((state: any) => state.Selection.accounts);
const dispatch = useDispatch();
useEffect(() => {
if (!accounts) {
dispatch(getDetails(id, otherid));
}
}, []);
if (!accounts) {
return <div>CircularLoader</div>;
}
if (!accounts.length) {
return <div>AccessUnavailablePanel</div>;
}
if (accounts.length === 1) {
return <div>Redirect</div>;
}
return <div>MyList</div>;
};
index.test.tsx:
import { MyFunctionalComponent } from './';
import createMockStore from 'redux-mock-store';
import { screen, render } from '#testing-library/react';
import { Provider } from 'react-redux';
import React from 'react';
const mockStore = createMockStore([]);
describe('69013562', () => {
test('should render AccessUnavailablePanel', () => {
const state = {
user: { id: '1', otherid: '2' },
Selection: { accounts: [] },
};
const store = mockStore(state);
render(<MyFunctionalComponent />, {
wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
});
expect(screen.getAllByText('AccessUnavailablePanel')).toBeTruthy();
expect(store.getActions()).toEqual([]);
});
test('should render CircularLoader and dispatch get details action', () => {
const state = {
user: { id: '1', otherid: '2' },
Selection: {},
};
const store = mockStore(state);
render(<MyFunctionalComponent />, {
wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
});
expect(screen.getAllByText('CircularLoader')).toBeTruthy();
expect(store.getActions()).toEqual([{ type: 'GET_DETAILS' }]);
});
test('should render Redirect', () => {
const state = {
user: { id: '1', otherid: '2' },
Selection: { accounts: [1] },
};
const store = mockStore(state);
render(<MyFunctionalComponent />, {
wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
});
expect(screen.getAllByText('Redirect')).toBeTruthy();
expect(store.getActions()).toEqual([]);
});
test('should render MyList', () => {
const state = {
user: { id: '1', otherid: '2' },
Selection: { accounts: [1, 2, 3] },
};
const store = mockStore(state);
render(<MyFunctionalComponent />, {
wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
});
expect(screen.getAllByText('MyList')).toBeTruthy();
expect(store.getActions()).toEqual([]);
});
});
test result:
PASS examples/69013562/index.test.tsx (10.158 s)
69013562
✓ should render AccessUnavailablePanel (28 ms)
✓ should render CircularLoader and dispatch get details action (3 ms)
✓ should render Redirect (2 ms)
✓ should render MyList (2 ms)
-----------|---------|----------|---------|---------|-------------------
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: 4 passed, 4 total
Snapshots: 0 total
Time: 11.014 s
There is no single answer to the test strategy, adjust according to the specific situation.
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
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 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