【问题标题】:Trying to use mock function when testing React component测试 React 组件时尝试使用模拟功能
【发布时间】:2019-12-10 23:32:05
【问题描述】:

我正在尝试学习测试,从基本测试开始,现在我想测试功能。但是,我浏览了所有感觉的互联网,但我无法测试 onClick 的东西。也许我什么都看不出来。

我尝试过使用 sinon.spy、jest 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"
  }

【问题讨论】:

  • 试试.find(Button)(不带引号)。字符串变体仅适用于 CSS 选择器,仅适用于 &lt;button&gt;` 等原生元素
  • 那么按钮是未定义的。如果我尝试 .find(#id),它的效果与 .find("Button") 相同。
  • 抱歉,我只是想尽快回复,以解决这个问题。是的,忘记导入了,但问题似乎还是一样。
  • 哦,抱歉,一开始没有完全阅读代码。我看不到您如何在组件中使用props.onClick。所以预计您的 jest.fn 在单击按钮后不会被调用 - 因为没有任何调用。
  • 但是在记录 Button.props() 时,里面有带有 onClick: [function handleShow] 的 props 对象。我认为它应该工作,但我只是不知道为什么..

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


【解决方案1】:

看,你的组件不期望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等嵌套元素上调用props与组件通信
  4. 验证渲染中发生的更改,例如 expect(wrapper.find(SomeNewElementShouldAppear)).toHaveLength(1)expect(wrapper.find(Modal).props().show).toEqual(true)
  5. 验证功能性道具是否已被调用,因为它应该被称为交互反应

【讨论】:

  • 非常感谢!我感到困惑,因为有很多代码是别人写的。
  • 添加了一种检查表来测试组件
猜你喜欢
  • 1970-01-01
  • 2021-12-10
  • 2021-09-04
  • 1970-01-01
  • 2021-10-29
  • 2021-03-28
  • 2021-04-25
  • 2015-10-21
  • 2019-07-09
相关资源
最近更新 更多