用Jest模拟React HOC时出现“无效的钩子调用”

时间:2020-06-13 14:10:52

标签: reactjs jestjs enzyme higher-order-components

我正在使用react-speech-recognition在我的React应用程序中将语音转录为文本。 react-speech-recognition提供了SpeechRecognition的高阶组件,这为封装的组件注入了browserSupportsSpeechRecognition之类的其他属性。

我的App组件如下:

// src/App.js
import React, { useEffect } from 'react';
import SpeechRecognition from 'react-speech-recognition';

const App = ({ transcript, browserSupportsSpeechRecognition }) => {
    useEffect(() => {
        console.log(`transcript changed: ${transcript}`);
    }, [transcript]);

    if (! browserSupportsSpeechRecognition) {
        return <span className="error">Speech recognition not supported</span>;
    }

    return <span className="transcript">{transcript}</span>;
};

const options = {
    autoStart: false,
    continuous: false
};

export default SpeechRecognition(options)(App);

我编写了一些测试来模拟支持语音识别的浏览器和不支持语音识别的浏览器:

// src/App.spec.js
import React from 'react';
import Enzyme, { mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import chai, { expect } from 'chai';
import chaiEnzyme from 'chai-enzyme';

chai.use(chaiEnzyme());

Enzyme.configure({ adapter: new Adapter() });

// Generate a mock SpeechRecognition HOC with the given props
function mockSpeechRecognition(mockProps) {
    return function(options) {
        return function(WrappedComponent) {
            return function(props) {
                return (
                    <WrappedComponent 
                        {...props}
                        {...mockProps}
                        recognition={{}}
                    />
                );
            };
        };
    };
}

describe('App component', () => {

    beforeEach(() => jest.resetModules());

    it('should show an error when speech recognition is not supported', () => {
        jest.mock('react-speech-recognition', () => mockSpeechRecognition({
            browserSupportsSpeechRecognition: false
        }));

        const App = require('./App').default;
        const wrapper = mount(<App />);

        expect(wrapper).to.contain.exactly(1).descendants('.error');
        expect(wrapper.find('.error'))
            .to.have.text('Speech recognition not supported');
    });

    it('should show the transcript when speech recognition is supported', () => {
        jest.mock('react-speech-recognition', () => mockSpeechRecognition({
            browserSupportsSpeechRecognition: true,
            transcript: 'foo'
        }));

        const App = require('./App').default;
        const wrapper = mount(<App />);

        expect(wrapper).to.contain.exactly(1).descendants('.transcript');
        expect(wrapper.find('.transcript')).to.have.text('foo');
    });

});

运行这些测试时,我得到一个"Invalid hook call" error,导致测试失败:

  ● App component › should show an error when speech recognition is not supported

    Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
    1. You might have mismatching versions of React and the renderer (such as React DOM)
    2. You might be breaking the Rules of Hooks
    3. You might have more than one copy of React in the same app
    See https://reactjs.org/warnings/invalid-hook-call-warning.html for tips about how to debug and fix this problem.

      3 | 
      4 | const App = ({ transcript, browserSupportsSpeechRecognition }) => {
    > 5 |     useEffect(() => {
        |     ^
      6 |         console.log(`transcript changed: ${transcript}`);
      7 |     }, [transcript]);
      8 | 

      at resolveDispatcher (node_modules/react/cjs/react.development.js:1465:13)
      at useEffect (node_modules/react/cjs/react.development.js:1508:20)
      at App (src/App.js:5:5)
      at renderWithHooks (node_modules/react-dom/cjs/react-dom.development.js:14803:18)
      at mountIndeterminateComponent (node_modules/react-dom/cjs/react-dom.development.js:17482:13)
      at beginWork (node_modules/react-dom/cjs/react-dom.development.js:18596:16)
      at HTMLUnknownElement.callCallback (node_modules/react-dom/cjs/react-dom.development.js:188:14)
      at invokeEventListeners (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:193:27)
      at HTMLUnknownElementImpl._dispatch (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:119:9)
      at HTMLUnknownElementImpl.dispatchEvent (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:82:17)
      at HTMLUnknownElementImpl.dispatchEvent (node_modules/jsdom/lib/jsdom/living/nodes/HTMLElement-impl.js:30:27)
      at HTMLUnknownElement.dispatchEvent (node_modules/jsdom/lib/jsdom/living/generated/EventTarget.js:157:21)
      at Object.invokeGuardedCallbackDev (node_modules/react-dom/cjs/react-dom.development.js:237:16)
      at invokeGuardedCallback (node_modules/react-dom/cjs/react-dom.development.js:292:31)
      at beginWork$1 (node_modules/react-dom/cjs/react-dom.development.js:23203:7)
      at performUnitOfWork (node_modules/react-dom/cjs/react-dom.development.js:22157:12)
      at workLoopSync (node_modules/react-dom/cjs/react-dom.development.js:22130:22)
      at performSyncWorkOnRoot (node_modules/react-dom/cjs/react-dom.development.js:21756:9)
      at scheduleUpdateOnFiber (node_modules/react-dom/cjs/react-dom.development.js:21188:7)
      at updateContainer (node_modules/react-dom/cjs/react-dom.development.js:24373:3)
      at node_modules/react-dom/cjs/react-dom.development.js:24758:7
      at unbatchedUpdates (node_modules/react-dom/cjs/react-dom.development.js:21903:12)
      at legacyRenderSubtreeIntoContainer (node_modules/react-dom/cjs/react-dom.development.js:24757:5)
      at Object.render (node_modules/react-dom/cjs/react-dom.development.js:24840:10)
      at fn (node_modules/enzyme-adapter-react-16/src/ReactSixteenAdapter.js:437:26)
      at node_modules/enzyme-adapter-react-16/src/ReactSixteenAdapter.js:354:37
      at batchedUpdates$1 (node_modules/react-dom/cjs/react-dom.development.js:21856:12)
      at Object.act (node_modules/react-dom/cjs/react-dom-test-utils.development.js:929:14)
      at wrapAct (node_modules/enzyme-adapter-react-16/src/ReactSixteenAdapter.js:354:13)
      at Object.render (node_modules/enzyme-adapter-react-16/src/ReactSixteenAdapter.js:423:16)
      at new ReactWrapper (node_modules/enzyme/src/ReactWrapper.js:115:16)
      at mount (node_modules/enzyme/src/mount.js:10:10)
      at Object.<anonymous> (src/App.spec.js:38:25)

  ● App component › should show the transcript when speech recognition is supported

    Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
    1. You might have mismatching versions of React and the renderer (such as React DOM)
    2. You might be breaking the Rules of Hooks
    3. You might have more than one copy of React in the same app
    See https://reactjs.org/warnings/invalid-hook-call-warning.html for tips about how to debug and fix this problem.

      3 | 
      4 | const App = ({ transcript, browserSupportsSpeechRecognition }) => {
    > 5 |     useEffect(() => {
        |     ^
      6 |         console.log(`transcript changed: ${transcript}`);
      7 |     }, [transcript]);
      8 | 

      at resolveDispatcher (node_modules/react/cjs/react.development.js:1465:13)
      at useEffect (node_modules/react/cjs/react.development.js:1508:20)
      at App (src/App.js:5:5)
      at renderWithHooks (node_modules/react-dom/cjs/react-dom.development.js:14803:18)
      at mountIndeterminateComponent (node_modules/react-dom/cjs/react-dom.development.js:17482:13)
      at beginWork (node_modules/react-dom/cjs/react-dom.development.js:18596:16)
      at HTMLUnknownElement.callCallback (node_modules/react-dom/cjs/react-dom.development.js:188:14)
      at invokeEventListeners (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:193:27)
      at HTMLUnknownElementImpl._dispatch (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:119:9)
      at HTMLUnknownElementImpl.dispatchEvent (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:82:17)
      at HTMLUnknownElementImpl.dispatchEvent (node_modules/jsdom/lib/jsdom/living/nodes/HTMLElement-impl.js:30:27)
      at HTMLUnknownElement.dispatchEvent (node_modules/jsdom/lib/jsdom/living/generated/EventTarget.js:157:21)
      at Object.invokeGuardedCallbackDev (node_modules/react-dom/cjs/react-dom.development.js:237:16)
      at invokeGuardedCallback (node_modules/react-dom/cjs/react-dom.development.js:292:31)
      at beginWork$1 (node_modules/react-dom/cjs/react-dom.development.js:23203:7)
      at performUnitOfWork (node_modules/react-dom/cjs/react-dom.development.js:22157:12)
      at workLoopSync (node_modules/react-dom/cjs/react-dom.development.js:22130:22)
      at performSyncWorkOnRoot (node_modules/react-dom/cjs/react-dom.development.js:21756:9)
      at scheduleUpdateOnFiber (node_modules/react-dom/cjs/react-dom.development.js:21188:7)
      at updateContainer (node_modules/react-dom/cjs/react-dom.development.js:24373:3)
      at node_modules/react-dom/cjs/react-dom.development.js:24758:7
      at unbatchedUpdates (node_modules/react-dom/cjs/react-dom.development.js:21903:12)
      at legacyRenderSubtreeIntoContainer (node_modules/react-dom/cjs/react-dom.development.js:24757:5)
      at Object.render (node_modules/react-dom/cjs/react-dom.development.js:24840:10)
      at fn (node_modules/enzyme-adapter-react-16/src/ReactSixteenAdapter.js:437:26)
      at node_modules/enzyme-adapter-react-16/src/ReactSixteenAdapter.js:354:37
      at batchedUpdates$1 (node_modules/react-dom/cjs/react-dom.development.js:21856:12)
      at Object.act (node_modules/react-dom/cjs/react-dom-test-utils.development.js:929:14)
      at wrapAct (node_modules/enzyme-adapter-react-16/src/ReactSixteenAdapter.js:354:13)
      at Object.render (node_modules/enzyme-adapter-react-16/src/ReactSixteenAdapter.js:423:16)
      at new ReactWrapper (node_modules/enzyme/src/ReactWrapper.js:115:16)
      at mount (node_modules/enzyme/src/mount.js:10:10)
      at Object.<anonymous> (src/App.spec.js:52:25)

但是,当我运行开发服务器并在浏览器中查看页面时,没有此类错误,并且可以看到useEffect钩子将消息记录到控制台。创建生产版本时也没有错误。我认为问题在于我如何嘲笑SpeechRecognition HOC。如果我删除了useEffect钩子,则测试通过。

这是一个从create-react-app开始的全新项目。我只有一份react和react-dom副本,并且版本匹配:

$ npm ls react react-dom
react-speech-recognition-invalid-hook-call@0.1.0 /Users/NMD/max_programming_projects/react-speech-recognition-invalid-hook-call
├── react@16.13.1 
└── react-dom@16.13.1

如何在测试中解决此错误?

3 个答案:

答案 0 :(得分:3)

这似乎是Jest中的错误:

Invalid hook call after `jest.resetModules` for dynamic `require`s

当您先调用jest.resetModulesjest.resetModuleRegistry然后在测试中require调用组件时,就会发生该错误。

您可以通过删除jest.resetModules / jest.resetModuleRegistry并将require包裹在对jest.isolateModules的调用中来解决此问题:

describe('App component', () => {

    it('should show an error when speech recognition is not supported', () => {
        jest.mock('react-speech-recognition', () => mockSpeechRecognition({
            browserSupportsSpeechRecognition: false
        }));

        jest.isolateModules(() => {
            const App = require('./App').default;
            const wrapper = mount(<App />);

            expect(wrapper).to.contain.exactly(1).descendants('.error');
            expect(wrapper.find('.error'))
                .to.have.text('Speech recognition not supported');
        });
    });

    it('should show the transcript when speech recognition is supported', () => {
        jest.mock('react-speech-recognition', () => mockSpeechRecognition({
            browserSupportsSpeechRecognition: true,
            transcript: 'foo'
        }));

        jest.isolateModules(() => {
            const App = require('./App').default;
            const wrapper = mount(<App />);

            expect(wrapper).to.contain.exactly(1).descendants('.transcript');
            expect(wrapper.find('.transcript')).to.have.text('foo');
        });
    });

});

运行此命令时,所有测试均通过,并且可以看到useEffect钩子的输出:

 PASS  src/App.spec.js
  App component
    ✓ should show an error when speech recognition is not supported (89ms)
    ✓ should show the transcript when speech recognition is supported (6ms)

  console.log src/App.js:6
    transcript changed: undefined

  console.log src/App.js:6
    transcript changed: foo

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        6.577s
Ran all test suites related to changed files.

答案 1 :(得分:1)

请参见enzyme github open issue

您几乎没有选择,但是通常需要通过传递browserSupportsSpeechRecognition来正确模拟您的语音识别,如下面的代码所示。

选项一

您可以嘲笑useEffect。只需编写代码来满足您在useEffect模拟中的需要。

describe("App component", () => {
  beforeEach(() => jest.resetModules());

  it("should show an error when speech recognition is not supported", () => {

    jest.mock("react", () => ({
      ...jest.requireActual("React"),
      useEffect: (f) => f(),
    }));

    jest.mock("react-speech-recognition", () => {
      return mockSpeechRecognition({ browserSupportsSpeechRecognition: false });
    });

    const App = require("./App").default;
    const wrapper = mount(<App transcript={"hi"} />);

    expect(wrapper).to.contain.exactly(1).descendants(".error");
    expect(wrapper.find(".error")).to.have.text(
      "Speech recognition not supported"
    );
  });

选项二

您实际上根本不需要嘲笑语音识别。这是开销。图书馆人员将自己进行测试。您可以命名导出App并将其导入并编写常规测试。

describe("App component - no mock", () => {
  beforeEach(() => jest.resetModules());

  it("should show an error when speech recognition is not supported", () => {
    const wrapper = mount(
      <App browserSupportsSpeechRecognition={false} transcript={"hi"} />
    );

    expect(wrapper).to.contain.exactly(1).descendants(".error");
    expect(wrapper.find(".error")).to.have.text(
      "Speech recognition not supported"
    );
  });

  it("should NOT show an error when speech recognition is not supported", () => {
    const wrapper = mount(
      <App browserSupportsSpeechRecognition={true} transcript={"hi"} />
    );

    expect(wrapper).does.not.contain.descendants(".error");
    // expect(wrapper.find(".error")).to.have.text(
    //   "Speech recognition not supported"
    // );
  });
});

选项三

使用反应测试库代替酶。


以上测试在本地运行,并且通过了

参考:

答案 2 :(得分:0)

您可以按照以下方式尝试模拟SpeechRecognition吗?

jest.mock('react-speech-recognition', () => ({
  __esModule: true, 
  default: mockSpeechRecognition({
    browserSupportsSpeechRecognition: false
  })
}));