Ad

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"
  }
Ad

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:

  1. pass props(better refer to component propTypes to find out that faster)
  2. spy on functional props by setting them as jest.fn
  3. communicate with component by calling props on nested elements like find(Button).props().onClick
  4. validate changes happened in render like expect(wrapper.find(SomeNewElementShouldAppear)).toHaveLength(1) or expect(wrapper.find(Modal).props().show).toEqual(true)
  5. validate functional props to have been called once it should be called as reaction on interaction
Ad
source: stackoverflow.com
Ad