Trying To Use Mock Function When Testing React Component
I'm trying to learn testing, started from basic tests, now I would like to test functions. However, I went through all of the internet it feels, and I couldn't manage to test onClick thing. Maybe I can't spot something.
I've tried to use sinon.spy, jest spyOn, etc. But none of these worked out.
Test, I'm trying to write.
import React from "react";
import {
shallow,
configure
} from "enzyme"
import Adapter from "enzyme-adapter-react-16";
import ConfirmDeleteButton from "../ConfirmDeleteButton"
import { Modal } from "react-bootstrap"
import sinon from "sinon"
import toJson from "enzyme-to-json";
configure({adapter: new Adapter()});
let wrapper;
const fnClick = sinon.spy();
// Assign component to wrapper variable, before all tests.
beforeEach(() => {
wrapper = shallow(<ConfirmDeleteButton onClick={fnClick} />)
});
describe("ConfirmDeleteButton component", () => {
it('should call mockfn onclick', () => {
//simulate onClick
wrapper
.find("Button").first()
.prop("onClick")()
expect(fnClick).toBeCalled();
})
})
Component, that I'm testing:
import React, { useState } from 'react'
import PropTypes from 'prop-types'
import { Button, Spinner, Modal } from 'react-bootstrap'
import { Mutation } from 'react-apollo'
const ConfirmDeleteButton = props => {
const [confirmDelete, setConfirmDelete] = useState(false)
const [timeout, setTimeoutState] = useState()
const [show, setShow] = useState(false)
const confirmButtonRef = React.createRef()
const handleClose = () => setShow(false)
const handleShow = () => setShow(true)
return (
<React.Fragment>
<Button variant='outline-danger' onClick={handleShow} id='openModal'>
Delete
</Button>
<Modal show={show} onHide={handleClose}>
<Modal.Header closeButton>
<Modal.Title>Confirmation dialog</Modal.Title>
</Modal.Header>
<Modal.Body>
Are you sure you want to delete this item?
This item will be gone permanently, and can cause cascade delete!
</Modal.Body>
<Modal.Footer>
<Button variant='outline-secondary' onClick={handleClose} id='closeModal'>
Close
</Button>
{confirmDelete ? (
<Mutation
mutation={props.deleteMutation}
// refetchQueries triggers parent component to be loaded with new data
refetchQueries={props.refetchQueries}
onCompleted={() => {
if (props.onCompleted) {
props.onCompleted()
}
setConfirmDelete(false)
}}
>
{(deleteFunction, { loading, error }) => (
<Button
variant='danger'
ref={confirmButtonRef}
onClick={() => {
let vars = {}
if (props.variables) {
for (let key in props.variables) {
vars[key] = props.variables[key]
}
} else {
vars['ID'] = props.id
}
deleteFunction({ variables: vars })
clearTimeout(timeout)
confirmButtonRef.current.blur()
handleClose()
}}
>
{loading ? <Spinner animation='border' as='span' size='sm' /> : 'Confirm'}
</Button>
)}
</Mutation>
) : (
<Button
variant='outline-success'
onClick={() => {
setConfirmDelete(true)
setTimeoutState(
setTimeout(function() {
setConfirmDelete(false)
}, 3000)
)
}}
>
Yes
</Button>
)}
</Modal.Footer>
</Modal>
</React.Fragment>
)
}
ConfirmDeleteButton.propTypes = {
onCompleted: PropTypes.func,
variables: PropTypes.object,
deleteMutation: PropTypes.object,
refetchQueries: PropTypes.array
}
export default ConfirmDeleteButton
In the end, I would like to get test: *if clicked button calls handleShow
Now I'm gettin :
ConfirmDeleteButton component › should call mockfn onclick
expect(jest.fn())[.not].toBeCalled()
Matcher error: received value must be a mock or spy function
Received has type: function
Received has value: [Function anonymous]
I Will add package.json just in case:
"devDependencies": {
"@babel/core": "^7.1.6",
"@babel/plugin-transform-runtime": "^7.5.0",
"@babel/preset-env": "^7.1.6",
"@babel/preset-es2015": "^7.0.0-beta.53",
"@babel/preset-react": "^7.0.0",
"@material-ui/core": "^4.2.0",
"apollo-boost": "^0.4.3",
"autoprefixer": "^9.6.1",
"babel-jest": "^24.8.0",
"babel-loader": "^8.0.6",
"babel-plugin-transform-export-extensions": "^6.22.0",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-stage-3": "^6.24.1",
"bootstrap": "^4.3.1",
"chai": "^4.2.0",
"chai-enzyme": "^1.0.0-beta.1",
"copy-webpack-plugin": "^5.0.3",
"css-loader": "^3.0.0",
"dotenv": "^8.0.0",
"dotenv-webpack": "^1.7.0",
"enzyme": "^3.10.0",
"enzyme-adapter-react-15": "^1.4.0",
"enzyme-adapter-react-16": "^1.14.0",
"enzyme-to-json": "^3.3.5",
"eslint": "^6.0.1",
"eslint-cli": "^1.1.1",
"eslint-config-airbnb": "^17.1.1",
"eslint-config-airbnb-base": "^13.2.0",
"eslint-config-prettier": "^6.0.0",
"eslint-loader": "^2.2.1",
"eslint-plugin-prettier": "^3.1.0",
"extract-text-webpack-plugin": "^3.0.2",
"file-loader": "^4.0.0",
"graphql": "^14.4.2",
"html-webpack-plugin": "^3.2.0",
"jest": "^24.8.0",
"mini-css-extract-plugin": "^0.7.0",
"node-sass": "^4.12.0",
"prettier": "^1.18.2",
"react": "^16.8.6",
"react-apollo": "^2.5.8",
"react-bootstrap": "^1.0.0-beta.9",
"react-dom": "^16.8.6",
"react-hot-loader": "^4.12.6",
"react-router-dom": "^5.0.1",
"react-scripts": "^3.0.1",
"react-test-renderer": "^16.8.6",
"sass-loader": "^7.1.0",
"sinon": "^7.3.2",
"sinon-chai": "^3.3.0",
"style-loader": "^0.23.1",
"ts-jest": "^24.0.2",
"uglifyjs-webpack-plugin": "^2.1.3",
"uuid": "^3.3.2",
"webpack": "^4.35.3",
"webpack-bundle-tracker": "^0.4.2-beta",
"webpack-cli": "^3.3.5",
"webpack-dev-server": "^3.7.2"
},
"dependencies": {
"@babel/core": "^7.5.4"
}
Answer
See, your component does not expect props.onClick
. You may notice that by looking into propTypes
and next verifying that in component's code. Since it is not used - it will never be called.
If you are looking how to test component you may verify Modal is shown after clicking first button
it('displays modal after clicking Delete button", async () => {
const wrapper = shallow(...);
expect(wrapper.find(Modal).props().show).toBeFalsy();
wrapper.find("#openModal").props().onClick();
// here internal state is changed and to get component re-rendered we just skip await
await Promise.resolve(); // below code runs after re-render
expect(wrapper.find(Modal).props().show).toBeTruthy();
});
To verify something with jest.fn
we need some prop
that is called inside the component.
Among
ConfirmDeleteButton.propTypes = {
onCompleted: PropTypes.func,
variables: PropTypes.object,
deleteMutation: PropTypes.object,
refetchQueries: PropTypes.array
}
only onCompleted
fits our needs. But it'd be harder to trigger it:
it('delegates onCompleted to Mutation after user confirmed deletion', async () => {
const onCompletedMock = jest.fn();
const wrapper = shallow(<ConfirmDeleteButton onCompleted={onCompletedMock} />);
wrapper.find("#openModal").props().onClick();
// below code runs after modal is opened
await Promise.resolve();
/* confirmation button does not have any id
so we need to take second button in scope of Modal */
wrapper.find(Modal).find(Button).at(1).props().onClick();
await Promise.resolve();
// at this moment <Mutation> should be rendered
expect(onCompletedMock).not.toHaveBeenCalled(); // not yet called
/* <Mutation /> will not call `onCompleted` on its own because of shallow rendering
so we need to call it manually */
wrapper.find(Mutation).props().onCompleted();
expect(onCompletedMock).toHaveBeenCalled(); // should be already called
})
In general:
- pass props(better refer to component
propTypes
to find out that faster) - spy on functional props by setting them as
jest.fn
- communicate with component by calling props on nested elements like
find(Button).props().onClick
- validate changes happened in render like
expect(wrapper.find(SomeNewElementShouldAppear)).toHaveLength(1)
orexpect(wrapper.find(Modal).props().show).toEqual(true)
- validate functional props to have been called once it should be called as reaction on interaction
Related Questions
- → Import statement and Babel
- → should I choose reactjs+f7 or f7+vue.js?
- → Uncaught TypeError: Cannot read property '__SECRET_DOM_DO_NOT_USE_OR_YOU_WILL_BE_FIRED' of undefined
- → .tsx webpack compile fails: Unexpected token <
- → React-router: Passing props to children
- → ListView.DataSource looping data for React Native
- → React Native with visual studio 2015 IDE
- → Can't test submit handler in React component
- → React + Flux - How to avoid global variable
- → Webpack, React & Babel, not rendering DOM
- → How do I determine if a new ReactJS session and/or Browser session has started?
- → Alt @decorators in React-Native
- → How to dynamically add class to parent div of focused input field?