在测试React组件时尝试使用模拟功能

时间:2019-08-02 10:35:11

标签: reactjs jestjs enzyme react-hooks react-bootstrap

我正在尝试从基础测试开始学习测试,现在我想测试功能。但是,我遍历了整个互联网,无法测试onClick。也许我找不到东西。

我尝试使用sinon.spy,开玩笑的spyOn等,但是这些都没有解决。

测试,我正在尝试写。

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();
  })
})

我正在测试的组件:

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

最后,我想进行测试: *如果单击的按钮将调用handleShow

现在我开始了:

 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]

我将添加package.json以防万一:

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

1 个答案:

答案 0 :(得分:0)

请参见,您的组件不希望使用props.onClick。通过查看propTypes,然后在组件代码中进行验证,您可能会注意到这一点。由于未使用-永远不会调用它。

如果您正在寻找如何测试组件的方法,则可以在单击第一个按钮之后验证Modal是否已显示

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();
});

要使用jest.fn进行验证,我们需要在组件内部调用的prop。 在

ConfirmDeleteButton.propTypes = {
  onCompleted: PropTypes.func,
  variables: PropTypes.object,
  deleteMutation: PropTypes.object,
  refetchQueries: PropTypes.array
}

onCompleted符合我们的需求。但是触发它会更困难:

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
})

通常:

  1. 传递道具(最好参考组件propTypes以便更快地找到它)
  2. 通过将功能道具设置为jest.fn来监视功能道具
  3. 通过调用find(Button).props().onClick之类的嵌套元素上的道具来与组件进行通信
  4. 验证expect(wrapper.find(SomeNewElementShouldAppear)).toHaveLength(1)expect(wrapper.find(Modal).props().show).toEqual(true)之类的渲染中发生的更改
  5. 验证功能道具一旦被调用为互动反应