【问题标题】:"Invalid hook call" when mocking React HOC with Jest使用 Jest 模拟 React HOC 时出现“无效的钩子调用”
【发布时间】:2020-10-03 06:05:30
【问题描述】:

我在我的 React 应用程序中使用 react-speech-recognition 将语音转录为文本。 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

如何在我的测试中修复这个错误?

【问题讨论】:

  • 最后,我从 react-speech-recognition 切换到了react-speech-kit,因为它的界面更好,不需要我做任何复杂的模拟。

标签: reactjs jestjs enzyme higher-order-components


【解决方案1】:

看起来这是 Jest 中的一个错误:

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

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

您可以通过删除jest.resetModules/jest.resetModuleRegistry 并将requires 包装在对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.

【讨论】:

【解决方案2】:

enzyme github open issue

您的选择很少,但通常您需要通过传递 browserSupportsSpeechRecognition 来正确模拟您的语音识别 hoc,如下面的代码所示。

选项一

你可以模拟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"
    );
  });

选项二

您实际上根本不需要模拟语音识别 hoc。这是一个开销。图书馆的人将自己进行测试。您可以对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"
    // );
  });
});

选项三

使用反应测试库代替酶。


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

参考资料:

【讨论】:

  • 感谢您的回答。嘲笑useEffect似乎有点奇怪;我不需要在我的任何其他测试中模拟 React 功能,那么为什么在这里呢?我也不想做选项#2,因为我不喜欢仅仅为了测试而改变我的代码;这对我来说有点代码味道。最终,这个问题看起来像是由bug in Jest 引起的。我写了一个描述一种解决方法的答案,但其他人可能更喜欢你的建议。再次感谢!
【解决方案3】:

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

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

【讨论】:

  • 感谢您的回答。我试了一下,但仍然收到无效的挂钩调用错误。
猜你喜欢
  • 2020-08-01
  • 2020-12-12
  • 2020-03-11
  • 2021-05-11
  • 1970-01-01
  • 2021-06-30
  • 1970-01-01
  • 2021-05-08
  • 2020-02-08
相关资源
最近更新 更多