即使React组件方法被调用,expect(...).toHaveBeenCalled()仍然失败

3
我正在尝试使用enzyme和jest测试React组件方法是否被调用。当<section>元素失焦(on blur)时,该函数应该被调用。该组件连接到Redux store,但也按名称导出,以将Redux从方程式中剔除。以下是组件的简化版本:
export class Section extends React.Component {
  constructor(props) {
    super(props)
    this.formRef = React.createRef();
    this.submitForm = this.submitForm.bind(this);
    this.state = {
      formSubmitted: false
    }
  }

  submitForm() {
    console.log("form submitted");
    this.setState({ formSubmitted: true })
    if (this.formRef && this.formRef.current) {
      this.formRef.current.submit();
    }
  }

  render() {
    return (
      <section onBlur={this.submitForm}>
        <form ref={this.formRef} action={url} method='post'>
          <input type="text" name="something" />
        </form>
      </section>
    );
  }
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Section);

我尝试了各种spyOn的组合,因为它似乎是问题的根源。但所有的尝试都失败了。

import React from 'react';
import { shallow } from 'enzyme';
import { Section } from './Section';

describe('Section', () => {
  it('should submit form on blur', () => {
    const wrapper = shallow(<Section content={{ body: [] }} />);
    const spy = spyOn(wrapper.instance(), 'submitForm');
    const spy2 = spyOn(Section.prototype, 'submitForm');
    const spy3 = jest.spyOn(wrapper.instance(), 'submitForm');
    const spy4 = jest.spyOn(Section.prototype, 'submitForm');

    wrapper.find('section').simulate('blur');
    expect(wrapper.state().formSubmitted).toEqual(true);
    expect(spy).toHaveBeenCalled();
    expect(spy2).toHaveBeenCalled();
    expect(spy3).toHaveBeenCalled();
    expect(spy4).toHaveBeenCalled();
  })
})

我给这个组件添加了一个状态并进行了测试,除了测试 expect(...).toHaveBeenCalled 以验证函数是否被实际调用,看起来是这样的。

在控制台中出现了 console.log('form submitted')

测试 expect(wrapper.state().formSubmitted).toEqual(true); 通过,这表明正确的函数被调用了。然而,我不希望为了测试而有一个不必要的状态。状态只是为了断言 "submitForm" 方法是否被调用而添加的。

所有的断言 expect(...).toHaveBeenCalled() 都失败,并显示错误信息 Expected spy to have been called, but it was not calledExpected mock function to have been called, but it was not called.

2个回答

4

很有趣,这个问题深入了解了JavaScript中一些比较不寻常的方面。


发生了什么

submitForm最初被定义为一个原型方法

class Section extends React.Component {
  ...
  submitForm() { ... }
  ...
}

这意味着你需要像这样创建 spy

jest.spyOn(Section.prototype, 'submitForm');

...但是它在constructor中被重新定义为一个实例属性

this.submitForm = this.submitForm.bind(this);

这意味着你现在需要像下面这样创建 spy

jest.spyOn(wrapper.instance(), 'submitForm');

...但是那样仍然不起作用,因为onBlurrender期间直接绑定到了this.submitForm

<section onBlur={this.submitForm}>
所以目前代码的写法实际上使得对submitForm进行监视不可能,因为创建spy需要一个instance,但这个instance在组件渲染之前是不可用的,并且onBlur在渲染期间直接绑定到this.submitForm


解决方法

onBlur更改为调用this.submitForm的函数,这样每当onBlur触发时,它都会调用this.submitForm当前值

render() {
  return (
    <section onBlur={() => this.submitForm()}>
      <form ref={this.formRef} action={url} method='post'>
        <input type="text" name="something" />
      </form>
    </section>
  );
}

当你在instance上使用spy代替submitForm时,onBlur触发时将调用spy

describe('Section', () => {
  it('should submit form on blur', () => {
    const wrapper = shallow(<Section content={{ body: [] }} />);
    const spy = jest.spyOn(wrapper.instance(), 'submitForm');

    wrapper.find('section').simulate('blur');
    expect(wrapper.state().formSubmitted).toEqual(true);
    expect(spy).toHaveBeenCalled();  // Success!
  })
})

在每次渲染时创建箭头函数可能会导致性能问题。 - Andrei Dotsenko
1
根据官方的 React 文档,@AndreiDotsenko 问:在渲染方法中使用箭头函数是否可以? "一般来说,是可以的"。如果您遇到性能问题,那么请移除箭头函数,但通常性能成本非常小。 - Brian Adams

1
实际上您想要验证当“Section”失去焦点时,表单的“submit()”是否被调用,对吗?因此,您不需要验证是否已调用内部方法-这不会确保表单是否已提交。此外,在重构的情况下,它会导致虚假负面结果(内部方法已重命名,但所有测试失败)。您可以代替模拟ref对象。然后,您将能够验证表单是否已提交。
it('should submit form on blur', () => {
  const wrapper = shallow(<Section content={{ body: [] }} />);
  wrapper.instance().formRef.current = { submit: jest.fn()};
  wrapper.find('section').simulate('blur');
  expect(wrapper.instance().formRef.current.submit).toHaveBeenCalled();
})

当然测试也依赖于组件的内部(包含引用的onto属性)。但我相信这样方式的测试更加可靠。


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