Jest spyOn函数被调用

79

我试图为一个简单的React组件编写一个简单的测试,并且我想使用Jest来确认当我使用enzyme模拟点击时,一个函数已经被调用。根据Jest文档,我应该能够使用spyOn来实现这一点:spyOn

然而,当我尝试这样做时,我不断收到TypeError: Cannot read property '_isMockFunction' of undefined的错误,这意味着我的spy未定义。我的代码看起来像这样:

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

有人能洞察我做错了什么吗?

4个回答

96

你几乎没有做任何更改就完成了,除了如何使用spyOn

使用 spy 时,有两个选项:spyOn App.prototype 或组件 component.instance()


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

将 spy 附加到类原型上的顺序以及呈现(浅层呈现)实例是重要的。

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组件的浅层渲染/挂载实例可用。本质上,spyOn只是在寻找要劫持并推到jest.fn()中的内容。它可以是普通对象:
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实例

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() 块时,我可能会选择第一种方法。如果我只是需要一个快速的 spy,则会使用第二种。注意附加 spy 的顺序。


编辑: 如果您想要检查您的 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");
*/

编辑: 以下是使用函数组件的示例。请记住,您在函数组件范围内定义的任何方法都不能被监视。您只能监视传递给函数组件的函数prop并测试其调用。此示例探讨了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*/);

如果一个函数组件是零元的(没有props或参数),那么你可以使用Jest来监视你期望从click方法中获得的任何效果。
import { myAction } from 'src/myActions'
function MyComponent() {
    const dispatch = useDispatch()
    const handleClick = (e) => dispatch(myAction('foobar'))
    return <button onClick={handleClick}>do it</button>
}

// Testing:
const { myAction } = require('src/myActions') // Grab effect actions or whatever file handles the effects.
jest.mock('src/myActions') // Mock the import

// Do the click
expect(myAction).toHaveBeenCalledWith('foobar')

1
@Byrd,我不确定你的意思。是jest不工作,spyOn不工作,还是其他问题?你的package.json是否正确配置了你如何配置jest?关于你的陈述有很多问题。 - taystack
如果问题是“如何使用A来完成B”,但你知道使用C是实现A的更好途径,那么回答C可能是合适的。我对spyOn没有意见,但在React组件中使用它来监视点击处理程序是99%情况下测试的垃圾方法。 - Alex Young
1
@AlexYoung 被监视的方法是任意的。到达该方法的路径也是任意的。示例代码存在缺陷,已经得到解决。最终,有人会使用 spyOn 来监视不接受 props 的组件,或者没有 state 的纯组件。对于我来说,监视一个 prototype 已经成功了100%。 - taystack
1
“或者 React.Component.prototype 后面的代码块是否表现出与第一个代码块不同的东西?” - ZAD-Man
1
@VictorCarvalho 这种技术不适用于功能组件。这里的目标是监视类方法,而功能组件没有这个功能。我建议你为你的用例研究 testing-library/react - taystack
显示剩余2条评论

31

你已经接近成功了。尽管我同意@Alex Young的答案,使用props可以实现这一点,但在试图窥探方法之前,你只需要引用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()
 })
})

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


4
在调用模拟点击之前,调用forceUpdate将spy函数附加到实例上:instance.forceUpdate() - Stefan Musarra
奇怪..我无法让上述代码在类似的测试中工作,但将应用程序渲染方法从“shallow”更改为“mount”可以解决问题。有什么想法为什么这可能是修复方法/为什么此测试也不需要“mount”? - youngrrrr
1
@youngrrrr 也许你的函数依赖于 DOM,而 shallow 不产生 DOM,而 mount 是一个完整的 DOM 渲染。 - 3stacks
1
缺少 forceUpdate 让我们感到困惑...不过这似乎很奇怪,有人能解释一下吗? - andy mccullough
这对我来说一直没有起作用 - 直到我意识到被测试的组件被一个高阶组件包装了。我需要使用 app.find("ActualComponent").instance() 来获取正确的实例,以便猴子补丁方法。 - Erin Drummond
1
你有任何想法为什么强制更新后它能够正常工作吗?这太神奇了! - Dehan

14

在你的测试代码中,你试图将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>
    );
  }
}

现在您可以将一个间谍函数作为组件的属性传递,并断言它被调用:

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

4
一个类是一个对象。spyOnClassName.prototype一起使用。 - taystack
2
一个类不是一个对象。在经典的面向对象编程中,它是一个对象的蓝图,在JavaScript中它是一个函数。typeof (class A {}) === "function"当我们实例化一个类时,我们创建一个基于该类原型的对象。类的原型是一个对象,如果需要,我们可以监视方法。最终,根据我在您答案下的评论,我们想要测试点击处理程序的效果,而不仅仅是它是否被调用。 - Alex Young

0

看起来最佳解决方案是避免测试这样的项目,而是测试正确调用了导入项的参数,或者测试是否显示了应该显示的内容(参见此其他答案的示例)。

但我仍然会提供一种“修复”问题的替代方法。在我的情况下,我使用的是一个功能组件,所以其他答案不起作用。我需要做的是将文件导入到自身中,并以这种方式调用有问题的函数...

// App.js
import * as thisModule from './App';

myClickFunc = () => {}

...
<p className="App-intro" onClick={thisModule.myClickFunc}>
  To get started, edit <code>src/App.js</code> and save to reload.
</p>

这是一个奇怪的问题(source1source2

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接