【问题标题】:Jest spyOn function calledJest spyOn 函数调用
【发布时间】:2017-11-29 20:46:08
【问题描述】:

我正在尝试为一个简单的 React 组件编写一个简单的测试,并且我想使用 Jest 来确认当我用酶模拟点击时已经调用了一个函数。根据 Jest 文档,我应该可以使用 spyOn 来执行此操作:spyOn

但是,当我尝试这个时,我不断收到TypeError: Cannot read property '_isMockFunction' of undefined,我认为这意味着我的间谍未定义。我的代码如下所示:

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
class App extends Component {

  myClickFunc = () => {
      console.log('clickity clickcty')
  }
  render() {
    return (
      <div className="App">
        <div className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h2>Welcome to React</h2>
        </div>
        <p className="App-intro" onClick={this.myClickFunc}>
          To get started, edit <code>src/App.js</code> and save to reload.
        </p>
      </div>
    );
  }
}

export default App;

在我的测试文件中:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { shallow, mount, render } from 'enzyme'

describe('my sweet test', () => {
 it('clicks it', () => {
    const spy = jest.spyOn(App, 'myClickFunc')
    const app = shallow(<App />)
    const p = app.find('.App-intro')
    p.simulate('click')
    expect(spy).toHaveBeenCalled()
 })
})

有人知道我做错了什么吗?

【问题讨论】:

    标签: javascript reactjs testing jestjs enzyme


    【解决方案1】:

    除了spyOn 之外,您几乎没有任何改变。 当你使用间谍时,你有两个选择:spyOnApp.prototype,或组件component.instance()


    const spy = jest.spyOn(Class.prototype, "method")

    将间谍附加到类原型和渲染(浅渲染)您的实例的顺序很重要。

    const spy = jest.spyOn(App.prototype, "myClickFn");
    const instance = shallow(<App />);
    

    第一行的App.prototype 位是使事情正常运行所需的。 JavaScript class 没有任何方法,除非您使用 new MyClass() 实例化它,或者您使用 MyClass.prototype。对于您的特定问题,您只需要监视App.prototype 方法myClickFn


    jest.spyOn(component.instance(), "method")

    const component = shallow(<App />);
    const spy = jest.spyOn(component.instance(), "myClickFn");
    

    此方法需要React.Componentshallow/render/mount 实例可用。本质上,spyOn 只是在寻找可以劫持并推入jest.fn() 的东西。可能是:

    一个普通的object

    const obj = {a: x => (true)};
    const spy = jest.spyOn(obj, "a");
    

    class:

    class Foo {
        bar() {}
    }
    
    const nope = jest.spyOn(Foo, "bar");
    // THROWS ERROR. Foo has no "bar" method.
    // Only an instance of Foo has "bar".
    const fooSpy = jest.spyOn(Foo.prototype, "bar");
    // Any call to "bar" will trigger this spy; prototype or instance
    
    const fooInstance = new Foo();
    const fooInstanceSpy = jest.spyOn(fooInstance, "bar");
    // Any call fooInstance makes to "bar" will trigger this spy.
    

    React.Component instance

    const component = shallow(<App />);
    /*
    component.instance()
    -> {myClickFn: f(), render: f(), ...etc}
    */
    const spy = jest.spyOn(component.instance(), "myClickFn");
    

    或者React.Component.prototype:

    /*
    App.prototype
    -> {myClickFn: f(), render: f(), ...etc}
    */
    const spy = jest.spyOn(App.prototype, "myClickFn");
    // Any call to "myClickFn" from any instance of App will trigger this spy.
    

    我已经使用并看到了这两种方法。当我有 beforeEach()beforeAll() 块时,我可能会采用第一种方法。如果我只需要一个快速的间谍,我会使用第二个。请注意附加间谍的顺序。


    编辑: 如果您想检查 myClickFn 的副作用,您可以在单独的测试中调用它。

    const app = shallow(<App />);
    app.instance().myClickFn()
    /*
    Now assert your function does what it is supposed to do...
    eg.
    expect(app.state("foo")).toEqual("bar");
    */
    

    编辑: 这是使用功能组件的示例。请记住,您的功能组件范围内的任何方法都不可用于间谍活动。您将监视传递给功能组件的功能道具并测试它们的调用。此示例探讨了 jest.fn()jest.spyOn 的使用,两者都共享模拟函数 API。虽然它没有回答最初的问题,但它仍然提供了对可能适用于与该问题间接相关的案例的其他技术的见解。

    function Component({ myClickFn, items }) {
       const handleClick = (id) => {
           return () => myClickFn(id);
       };
       return (<>
           {items.map(({id, name}) => (
               <div key={id} onClick={handleClick(id)}>{name}</div>
           ))}
       </>);
    }
    
    const props = { myClickFn: jest.fn(), items: [/*...{id, name}*/] };
    const component = render(<Component {...props} />);
    // Do stuff to fire a click event
    expect(props.myClickFn).toHaveBeenCalledWith(/*whatever*/);
    

    【讨论】:

    • @Byrd 我不确定你的意思。是开玩笑不工作,spyOn 不工作,还是有什么不同?您的 package.json 是否正确配置为您配置 jest 的方式?关于你的陈述有很多问题。
    • @AlexYoung 被监视的方法是任意的。到达该方法的路径是任意的。示例代码存在缺陷并已得到解决。最终,有人将有一个spyOn 的用例,其中包含不接受props 的组件,或者没有state 的纯组件。监视prototype 对我来说是 100% 成功的。
    • “Or a React.Component.prototype”之后的代码块与第一个代码块有什么不同吗?
    • @VictorCarvalho 这种技术不适用于功能组件。这里的目标是监视类方法,而功能组件没有这些方法。我建议为您的用例研究testing-library/react
    • .prototype!!你拯救了我的一天!
    【解决方案2】:

    你快到了。尽管我同意@Alex Young 关于为此使用道具的回答,但您只需要在尝试监视该方法之前参考instance

    describe('my sweet test', () => {
     it('clicks it', () => {
        const app = shallow(<App />)
        const instance = app.instance()
        const spy = jest.spyOn(instance, 'myClickFunc')
    
        instance.forceUpdate();    
    
        const p = app.find('.App-intro')
        p.simulate('click')
        expect(spy).toHaveBeenCalled()
     })
    })
    

    文档: http://airbnb.io/enzyme/docs/api/ShallowWrapper/instance.html

    【讨论】:

    • 在调用模拟点击之前,调用forceUpdate将spy函数附加到实例:instance.forceUpdate()
    • 奇怪.. 我无法让上述方法进行类似的测试,但将应用程序渲染方法从“浅”更改为“安装”修复了它。任何想法为什么这可能是修复/为什么此测试不需要“安装”?
    • @youngrrrr 也许你的函数依赖于 DOM,浅层没有产品,而 mount 是一个完整的 DOM 渲染
    • 失踪的forceUpdate 抓住了我们……不过看起来很奇怪,谁能解释一下?
    • 知道为什么当我们强制更新时这会起作用:O。太可怕了!
    【解决方案3】:

    在您的测试代码中,您试图将App 传递给 spyOn 函数,但 spyOn 仅适用于对象,而不适用于类。通常,您需要在此处使用以下两种方法之一:

    1) 点击处理程序调用作为道具传递的函数,例如

    class App extends Component {
    
      myClickFunc = () => {
          console.log('clickity clickcty');
          this.props.someCallback();
      }
      render() {
        return (
          <div className="App">
            <div className="App-header">
              <img src={logo} className="App-logo" alt="logo" />
              <h2>Welcome to React</h2>
            </div>
            <p className="App-intro" onClick={this.myClickFunc}>
              To get started, edit <code>src/App.js</code> and save to reload.
            </p>
          </div>
        );
      }
    }
    

    您现在可以将 spy 函数作为 prop 传递给组件,并断言它被调用:

    describe('my sweet test', () => {
     it('clicks it', () => {
        const spy = jest.fn();
        const app = shallow(<App someCallback={spy} />)
        const p = app.find('.App-intro')
        p.simulate('click')
        expect(spy).toHaveBeenCalled()
     })
    })
    

    2) 点击处理程序在组件上设置某些状态,例如

    class App extends Component {
      state = {
          aProperty: 'first'
      }
    
      myClickFunc = () => {
          console.log('clickity clickcty');
          this.setState({
              aProperty: 'second'
          });
      }
      render() {
        return (
          <div className="App">
            <div className="App-header">
              <img src={logo} className="App-logo" alt="logo" />
              <h2>Welcome to React</h2>
            </div>
            <p className="App-intro" onClick={this.myClickFunc}>
              To get started, edit <code>src/App.js</code> and save to reload.
            </p>
          </div>
        );
      }
    }
    

    您现在可以对组件的状态进行断言,即

    describe('my sweet test', () => {
     it('clicks it', () => {
        const app = shallow(<App />)
        const p = app.find('.App-intro')
        p.simulate('click')
        expect(app.state('aProperty')).toEqual('second');
     })
    })
    

    【讨论】:

    • 一个类是一个对象。 spyOnClassName.prototype 一起使用
    • 类不是对象。在经典的 OO 中,它是一个对象的蓝图,在 JavaScript 中,它是一个函数。 typeof (class A {}) === "function" 当我们实例化一个类时,我们会根据类的原型创建一个对象。类的原型是一个对象,如果我们愿意,我们可以监视方法。最终,根据我的 cmets 在您的回答下,我们想要测试点击处理程序的效果,而不仅仅是它已被调用。
    猜你喜欢
    • 2021-08-21
    • 2021-11-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-03-14
    • 1970-01-01
    • 2020-06-02
    • 1970-01-01
    相关资源
    最近更新 更多