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
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'm new to Jest and I'm writing test case for the below function,
useEffect(() => {
fetch("http://ip-api.com/json")
.then(res => res.json())
.then(data => {
cntCode = `country_code=${data.countryCode}`;
country = `country=${data.country}`;
});
});
I tried few ways to cover using but I'm not able to figure out how to cover this function. Can someone help me in writing the testcase for this please?
Here is the unit test solution:
index.tsx:
import React, { useEffect, useState } from 'react';
export const MyComponent = () => {
const [cntCode, setCntCode] = useState('');
const [country, setCountry] = useState('');
useEffect(() => {
fetch('http://ip-api.com/json')
.then((res) => res.json())
.then((data) => {
setCntCode(data.countryCode);
setCountry(data.country);
});
}, [cntCode, country]);
return (
<div>
country: {country}, cntCode: {cntCode}
</div>
);
};
index.test.ts:
import { MyComponent } from './';
import React from 'react';
import { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
describe('MyComponent', () => {
afterEach(() => {
jest.resetAllMocks();
});
it('should pass', async () => {
const mResponse = jest.fn().mockResolvedValue({ countryCode: 123, country: 'US' });
(global as any).fetch = jest.fn(() => {
return Promise.resolve({ json: mResponse });
});
const wrapper = mount(<MyComponent></MyComponent>);
expect(wrapper.exists()).toBeTruthy();
expect(wrapper.text()).toBe('country: , cntCode: ');
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});
expect(wrapper.text()).toBe('country: US, cntCode: 123');
expect((global as any).fetch).toBeCalledWith('http://ip-api.com/json');
});
});
Unit test result with 100% coverage:
PASS src/stackoverflow/59368499/index.test.tsx (9.629s)
MyComponent
✓ should pass (73ms)
-----------|----------|----------|----------|----------|-------------------|
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.64s
Source code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/59368499
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