I have such a code in componentDidMount:
async componentDidMount() {
window.scrollTo(0, 0);
try {
// check if user has auto
const { vehicles: auto } = await api.getVehicles();
const items = auto.filter((car) => car.active);
if (items.length) {
this.setActiveAuto(items);
} else {
const { name } = await api.getCurrentMoto();
this.setState({
name
});
}
} catch (err) {
console.log(err);
}
}
I want to test correct state update in getCurrentMoto() so I wrote such a test:
const motoResponse = {
name: 'Honda'
};
const autoResponse = {
vehicles: [],
};
jest.mock('../api/api.js');
describe('<Component /> Component render', () => {
let wrapper;
beforeEach(() => {
api.getVehicles.mockResolvedValueOnce(autoResponse);
api.getCurrentMoto.mockResolvedValueOnce(motoResponse);
wrapper = shallow(<Component />);
});
test('correct data passed from endpoint to state in componentDidMount', () => {
expect(wrapper.state().name).toBe('Honda');
});
});
but my test failed
Expected: "Honda"
Received: ""
It seems like mocked api.getCurrentMoto.mockResolvedValueOnce(motoResponse); didn't call and I cannot understand how to fix that or change test to test correct state update in componentDidMount
Here is the solution:
index.tsx:
import React, { Component } from 'react';
import { api } from './api';
class XComponent extends Component {
constructor(props) {
super(props);
this.state = {
name: ''
};
}
public async componentDidMount() {
console.log('componentDidMount');
window.scrollTo(0, 0);
try {
// check if user has auto
const { vehicles: auto } = await api.getVehicles();
const items = auto.filter(car => car.active);
if (items.length) {
this.setActiveAuto(items);
} else {
const { name } = await api.getCurrentMoto();
this.setState({
name
});
}
} catch (err) {
console.log(err);
}
}
public setActiveAuto(items) {
//
}
public render() {
return <div>58082922</div>;
}
}
export default XComponent;
api.ts:
export const api = {
async getVehicles() {
return { vehicles: [{ active: true }] };
},
async getCurrentMoto() {
return { name: 'real name' };
}
};
index.spec.tsx:
import React from 'react';
import XComponent from './';
import { api } from './api';
import { shallow } from 'enzyme';
jest.mock('./api.ts');
window.scrollTo = jest.fn();
const motoResponse = {
name: 'Honda'
};
const autoResponse = {
vehicles: []
};
describe('<Component /> Component render', () => {
let wrapper;
beforeEach(() => {
(api.getVehicles as any).mockResolvedValueOnce(autoResponse);
(api.getCurrentMoto as any).mockResolvedValueOnce(motoResponse);
wrapper = shallow(<XComponent />);
});
test('correct data passed from endpoint to state in componentDidMount', done => {
expect.assertions(5);
expect(wrapper.text()).toBe('58082922');
setImmediate(() => {
expect(api.getVehicles).toBeCalledTimes(1);
expect(api.getCurrentMoto).toBeCalledTimes(1);
expect(window.scrollTo).toBeCalledWith(0, 0);
expect(wrapper.state().name).toBe('Honda');
done();
});
});
});
Unit test result with coverage report:
PASS src/stackoverflow/58082922/index.spec.tsx
<Component /> Component render
✓ correct data passed from endpoint to state in componentDidMount (43ms)
console.log src/stackoverflow/58082922/index.tsx:4305
componentDidMount
-----------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
-----------|----------|----------|----------|----------|-------------------|
All files | 81.48 | 80 | 50 | 82.61 | |
api.ts | 33.33 | 100 | 0 | 33.33 | 3,7 |
index.tsx | 87.5 | 80 | 66.67 | 90 | 21,30 |
-----------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 6.908s, estimated 11s
Source code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/58082922
Related
I want to simulate clicking a button (#reset), button has onClick fetching with axios. I wan to mock this data, but I'm getting an error: "Cannot read property 'then' of undefined"
Test:
import React from "react";
import PersonsList from "./Components/PersonsList/PersonsList";
import * as axios from "axios";
import { shallow } from "enzyme";
const mockData = [
{
gender: "female",
},
];
jest.mock("axios");
describe("axios", () => {
let wrapper;
beforeEach(() => {
wrapper = shallow(<PersonsList />);
});
axios.get.mockResolvedValue({ data: mockData });
it("reset", () => {
const resetButton = wrapper.find("#reset");
resetButton.simulate("click");
});
});
button:
<button
className="actions__modify-list"
onClick={() => {
getPeople();
}}
id="reset"
>
Function called on button click:
const getPeople = () => {
const url = "https://randomuser.me/api/?results=10";
axios(url)
.then((res) => {
setPeople(res.data.results);
})
.catch(() => {
setError(true);
});
};
You should mock resolved value for axios(), NOT axios.get() method.
E.g.
PersonsList.jsx:
import axios from 'axios';
import React, { useState } from 'react';
export function PersonsList() {
const [person, setPeople] = useState();
const [error, setError] = useState(false);
const getPeople = () => {
const url = 'https://randomuser.me/api/?results=10';
axios(url)
.then((res) => {
setPeople(res.data.results);
})
.catch(() => {
setError(true);
});
};
return (
<div>
<button
className="actions__modify-list"
onClick={() => {
getPeople();
}}
id="reset"
></button>
</div>
);
}
PersonsList.test.jsx:
import { PersonsList } from './PersonsList';
import axios from 'axios';
import React from 'react';
import { shallow } from 'enzyme';
jest.mock('axios');
const mockData = [{ gender: 'female' }];
describe('68393613', () => {
let wrapper;
beforeEach(() => {
wrapper = shallow(<PersonsList />);
});
test('should pass', () => {
axios.mockResolvedValue({ data: mockData });
const resetButton = wrapper.find('#reset');
resetButton.simulate('click');
});
});
test result:
PASS examples/68393613/PersonsList.test.jsx (8.311 s)
68393613
✓ should pass (12 ms)
-----------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------------|---------|----------|---------|---------|-------------------
All files | 91.67 | 100 | 80 | 91.67 |
PersonsList.jsx | 91.67 | 100 | 80 | 91.67 | 15
-----------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 8.837 s, estimated 11 s
This is my react HOC:
import React from "react"
import { getData } from 'services/DataService'
export function withData(WrappedComponent, count) {
return class extends React.Component {
state = {
data: []
}
async componentDidMount() {
this.setState({
data: await getData(count)
})
}
render() {
const { data } = this.state
return data.length > 0 && <WrappedComponent data={data} />
}
}
}
I want to write a unit test to prove that if getData(count) returns some data, the WrappedComponent is correctly rendered OR at least state.data.length is greater than zero.
I mocked the getData like this in my test with a valid mockResponse:
jest.mock("services/DataService", () => ({
getData: jest.fn().mockImplementation(() => Promise.resolve(mockResponse))
}))
But I am stuck at this point. Community has some answers about testing components having an async componentDidMount, but none of them are related with HOCs. I tried things like this:
const WithReleaseNotesComponent = withReleaseNotes(mockedComponent)
and then trying to instantiate or mount WithReleaseNotesComponent, but I've never seen they have right state/props that I can test against. state/props are always empty. What am I missing?
I will use the enzyme library to test your components. In order to ensure that the asynchronous task execution of the componentDidMount life cycle method using async/await syntax is completed, we need to flush the promise queue.
For more info about how does flush promise queue doing, see macrotasks-and-microtasks
All microtasks are completed before any other event handling or rendering or any other macrotask takes place.
Which means the async/await and promise are microtasks that they will be completed before macrotask created by setTimeout.
E.g.
index.tsx:
import React from 'react';
import { getData } from './services/DataService';
export function withData(WrappedComponent, count) {
return class extends React.Component {
state = {
data: [],
};
async componentDidMount() {
this.setState({
data: await getData(count),
});
}
render() {
const { data } = this.state;
return data.length > 0 && <WrappedComponent data={data} />;
}
};
}
index.test.tsx:
import React from 'react';
import { withData } from './';
import { getData } from './services/DataService';
import { shallow } from 'enzyme';
import { mocked } from 'ts-jest/utils';
jest.mock('./services/DataService');
const mockedGetData = mocked(getData);
class MockedComponent extends React.Component {
render() {
return <div>mocked component</div>;
}
}
function flushPromises() {
return new Promise((resolve) => setTimeout(resolve, 0));
}
describe('66260393', () => {
afterAll(() => {
jest.resetAllMocks();
});
it('should render wrapped component', async () => {
mockedGetData.mockResolvedValueOnce('fake data');
const WithDataComponent = withData(MockedComponent, 1);
const wrapper = shallow(<WithDataComponent></WithDataComponent>);
expect(wrapper.state('data')).toEqual([]);
await flushPromises();
expect(wrapper.state('data')).toEqual('fake data');
expect(wrapper.find(MockedComponent)).toBeTruthy();
expect(wrapper.find(MockedComponent).prop('data')).toEqual('fake data');
});
});
unit test result:
PASS examples/66260393/index.test.tsx
66260393
✓ should render wrapped component (23 ms)
-------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-------------------|---------|----------|---------|---------|-------------------
All files | 90 | 100 | 80 | 90 |
66260393 | 100 | 100 | 100 | 100 |
index.tsx | 100 | 100 | 100 | 100 |
66260393/services | 50 | 100 | 0 | 50 |
DataService.ts | 50 | 100 | 0 | 50 | 2
-------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 3.198 s
The problem was my mock. I don't know why, but jest.mock() didn't work as expected. I replaced it with a jest.spyOn() and then called shallow with await. Everything worked out perfectly. There wasn't even any need to flush promises.
withDataHOC.spec.js
import React from 'react'
import { shallow } from 'enzyme'
import { withData } from 'components/higher-order-components/WithDataHOC'
import * as dataService from 'services/DataService'
describe("WithDataHOC", () => {
afterAll(() => {
jest.resetAllMocks()
})
const mockedComponent = jest.fn()
it("doesn't render anything if there are no data", async() => {
jest.spyOn(dataService, "getData").mockImplementation( () => Promise.resolve())
const WithDataComponent = withData(mockedComponent)
const wrapper = shallow(<WithDataComponent />)
expect(wrapper).toMatchSnapshot()
})
const mockResponse = JSON.parse(require('services/__mocks__/DataResponse').dataResponseCache)
it("renders correctly if there are data", async() => {
jest.spyOn(dataService, "getData").mockImplementation( () => Promise.resolve(mockResponse))
const WithDataComponent = withData(mockedComponent)
const wrapper = await shallow(<WithDataComponent />)
expect(wrapper).toMatchSnapshot()
})
})
import { logout } from '../../actions/user.actions';
const Logout = () => {
useEffect(() => {
Router.push('/any');
}, []);
};
Test File
afterEach(() => {
jest.clearAllMocks();
});
afterAll(() => {
jest.resetAllMocks(); // clear all the mocks.
});
it('Should check the route', () => {
const wrapper = mount(<Logout />);
expect(wrapper.find('LoadingMessage')).toBeTruthy();
expect(useDispatch).toBeCalledTimes(1);
const mDispatch = useDispatch();
expect(mDispatch).toBeCalledWith({ type: 'USER_LOGOUT' });
expect(Router.push).toBeCalledWith('/');expect(mDispatch).toBeCalledWith({ type: 'USER_LOGOUT' });
expect(mDispatch).toBeCalledWith('/');
})
})
This is my sample of code that I have written, I want to write a test case for this. So I can test the Router push is happening or not?
Wanting some idea or help.
Thanks in advance.
Below unit test solution mocks useDispatch hook and next/router module without using a mock redux store. Using the mocked redux store and check the state of store after dispatching an action is closer to integration testing.
E.g.
index.jsx:
import React, { useEffect } from 'react';
import Router from 'next/router';
import { useDispatch } from 'react-redux';
import { logout } from './user.actions';
import { LoadingMessage } from './LoadingMessage';
export const Logout = () => {
const dispatch = useDispatch();
const props = {
active: true,
withOverlay: false,
small: false,
};
useEffect(() => {
dispatch(logout());
Router.push('/');
}, []);
return (
<div>
<LoadingMessage {...props}>
<div>Logging out, please wait...</div>
</LoadingMessage>
</div>
);
};
user.actions.ts:
export function logout() {
return {
type: 'LOGOUT',
};
}
LoadingMessage.jsx
export const LoadingMessage = ({ children }) => <div>{children}</div>;
index.test.jsx:
import { Logout } from './';
import { useDispatch } from 'react-redux';
import { mount } from 'enzyme';
import Router from 'next/router';
jest.mock('next/router', () => ({ push: jest.fn() }), { virtual: true });
jest.mock('react-redux', () => {
const originalReactRedux = jest.requireActual('react-redux');
const mDispatch = jest.fn();
const mUseDispatch = jest.fn(() => mDispatch);
return {
...originalReactRedux,
useDispatch: mUseDispatch,
};
});
describe('61928263', () => {
afterEach(() => {
jest.clearAllMocks();
});
afterAll(() => {
jest.resetAllMocks();
});
it('should pass without using mock store', () => {
const wrapper = mount(<Logout></Logout>);
expect(wrapper.find('LoadingMessage')).toBeTruthy();
expect(useDispatch).toBeCalledTimes(1);
const mDispatch = useDispatch(); // get the mocked dispatch function
expect(mDispatch).toBeCalledWith({ type: 'LOGOUT' });
expect(Router.push).toBeCalledWith('/');
});
});
The outcome for the test:
PASS stackoverflow/61928263/index.test.jsx (8.62s)
61928263
✓ should pass without using mock store (37ms)
--------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
--------------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
LoadingMessage.jsx | 100 | 100 | 100 | 100 |
index.jsx | 100 | 100 | 100 | 100 |
user.actions.ts | 100 | 100 | 100 | 100 |
--------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 9.601s
I am pretty new to the React world. Here's what I have in react-hooks.
export default function MyComponent() {
const [data, setData] = useState(null);
useEffect( () => {
getData().then(setData)
}, []);
return(
data ? <AnotherComponent /> : <LoadingComponent />
);
}
getData() is in another component with 'fetch'.
export function getData() {
return fetch('/api/v2/user').then(response => {
if(response.status === 200) {
return response.json()
} else {
return {};
}
});
}
I am using Jest/Enzyme as a testing framework and want to test the scenario with mock data so I can test LoadingComponent is not present in DOM. This is what I am trying but it seems mock values are not being returned.
const mockValues = {
data: {}
count: 10
result: []
};
jest.mock('../DataService');
const mockService = require('../DataService');
mockService.getData = jest.fn().mockResolvedValue(mockValues);
...
const component = mount(<MyComponent />);
expect(component.html()).toEqual(expect.not.stringContaining('LoadingComponent'));
I see the "mount" works fine but it seems mock values are not being returned and hence LoadingComponent is present.
Here is the unit test solution:
index.tsx:
import React, { useState, useEffect } from 'react';
import { AnotherComponent } from './AnotherComponent';
import { LoadingComponent } from './LoadingComponent';
import { getData } from './dataService';
export default function MyComponent() {
const [data, setData] = useState(null);
useEffect(() => {
getData().then(setData);
}, []);
return data ? <AnotherComponent /> : <LoadingComponent />;
}
AnotherComponent.jsx:
export const AnotherComponent = () => <div>AnotherComponent</div>;
LoadingComponent.jsx:
export const LoadingComponent = () => <div>LoadingComponent</div>;
dataService.js:
export function getData() {
return fetch('/api/v2/user').then((response) => {
if (response.status === 200) {
return response.json();
} else {
return {};
}
});
}
index.test.jsx:
import React from 'react';
import * as DataService from './dataService';
import { mount } from 'enzyme';
import MyComponent from './';
import { act } from 'react-dom/test-utils';
jest.mock('./dataService');
const whenStable = async () => {
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});
};
describe('60913717', () => {
it('should pass', async () => {
const mockValues = {
data: {},
count: 10,
result: [],
};
DataService.getData = jest.fn().mockResolvedValueOnce(mockValues);
const wrapper = mount(<MyComponent></MyComponent>);
expect(wrapper.find('LoadingComponent')).toBeTruthy();
await whenStable();
expect(wrapper.find('AnotherComponent')).toBeTruthy();
});
});
unit test results with coverage report:
PASS stackoverflow/60913717/index.test.jsx (28.041s)
60913717
✓ should pass (48ms)
----------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------------------|---------|----------|---------|---------|-------------------
All files | 80 | 50 | 66.67 | 75 |
AnotherComponent.jsx | 100 | 100 | 100 | 100 |
LoadingComponent.jsx | 100 | 100 | 100 | 100 |
dataService.js | 20 | 0 | 0 | 20 | 2-4,6
index.jsx | 100 | 100 | 100 | 100 |
----------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 29.721s
source code: https://github.com/mrdulin/react-apollo-graphql-starter-kit/tree/master/stackoverflow/60913717
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