【问题标题】:How to mock/spy useState hook in jest?如何在玩笑中模拟/监视 useState 钩子?
【发布时间】:2021-01-17 18:03:32
【问题描述】:

我试图监视 useState React 钩子,但我总是测试失败

这是我的 React 组件:

const Counter= () => {
    const[counter, setCounter] = useState(0);

    const handleClick=() => {
        setCounter(counter + 1);
    }

    return (
        <div>
            <h2>{counter}</h2>
            <button onClick={handleClick} id="button">increment</button>
        </div>
    )
}

counter.test.js

it('increment counter correctlry', () => {
    let wrapper = shallow(<Counter/>);
    const setState = jest.fn();
    const useStateSpy = jest.spyOn(React, 'useState');

    useStateSpy.mockImplementation((init) => [init, setState]);
     const button = wrapper.find("button")
     button.simulate('click');
     expect(setState).toHaveBeenCalledWith(1);
})

不幸的是,这不起作用,我得到了测试失败的消息:

expected 1
Number of calls: 0

【问题讨论】:

  • 我同意你的问题更简单。但是,只需在 stackoverflow 中搜索 'mock usestate' 并查看有关此的许多其他问题。链接问题中您的问题的答案也可能适用。
  • 好吧,有一条关于要点的评论说它可以工作gist.github.com/agiveygives/…你只需要使用React.useState()
  • 评论说使用React.useState()而不是导入{ useState } from 'react'
  • 另外,你在嘲笑useState,所以你不应该期望它真正更新状态,你只能检查它是否被调用
  • 我真的不知道,让我研究一下原因,我会添加答案,以便以后来的人可以学到新的东西

标签: javascript reactjs testing jestjs enzyme


【解决方案1】:

您需要使用React.useState 而不是单一导入useState

我认为是关于代码如何获取 transpiled,正如您在 babel repl 中看到的那样,来自单个导入的 useState 最终与模块导入中的一个不同

_react.useState // useState
_react.default.useState // React.useState;

所以你监视_react.default.useState,但你的组件使用_react.useState。 似乎不可能监视单个导入,因为您需要该函数属于一个对象,这是一个非常广泛的指南,解释了模拟/监视模块的方式https://github.com/HugoDF/mock-spy-module-import

正如@Alex Mackay 提到的,你可能想改变你对测试反应组件的心态,建议转向反应测试库,但如果你真的需要坚持使用酶,你不需要走那么远至于模拟反应库本身

【讨论】:

  • 您可能需要模拟useState 不是为了知道它是否被调用,而是为了防止在调用useState 时出现控制台上的错误和警告(如wrap your component in act())和其他问题。因此,模拟它以仅返回受控制的哑数据是防止这些问题的有效方法。
【解决方案2】:

diedu 的回答让我找到了正确的方向,我想出了这个解决方案:

  1. 从 react 中模拟使用状态以返回 jest.fn() 作为 useState: 1.1 同样在之后立即导入 useState - 现在将是 e jest mock(从 jest.fn() 调用返回)

jest.mock('react', ()=>({
  ...jest.requireActual('react'),
  useState: jest.fn()
}))
import { useState } from 'react';
  1. 稍后在 beforeEach 中,将其设置为原始 useState,以便在所有需要它不被模拟的情况下

describe("Test", ()=>{
  beforeEach(()=>{
    useState.mockImplementation(jest.requireActual('react').useState);
    //other preperations
  })
  //tests
})
  1. 在测试本身中根据需要对其进行模拟:

it("Actual test", ()=>{
  useState.mockImplementation(()=>["someMockedValue", someMockOrSpySetter])
})

临别笔记:虽然在“黑匣子”中弄脏你的手在概念上可能有些错误,其中之一是单元测试,但有时这样做确实非常有用。

【讨论】:

    【解决方案3】:

    令人讨厌的是 Codesandbox 目前的测试模块有问题,所以我无法发布一个工作示例,但我会尝试解释为什么模拟 useState 通常是一件坏事。

    用户不关心是否调用了useState,他们关心的是当我单击递增时,计数应该增加一,因此这就是您应该测试的内容。

    // App
    import React, { useState } from "react";
    export default function App() {
      const [count, setCount] = useState(0);
      return (
        <div>
          <h1>Count: {count}</h1>
          <button onClick={() => setCount((prev) => prev + 1)}>Increment</button>
        </div>
      );
    }
    // Tests
    import React from "react";
    import App from "./App";
    import { screen, render } from "@testing-library/react";
    import userEvent from "@testing-library/user-event";
    
    describe("App should", () => {
      it('increment count value when "Increment" btn clicked', () => {
        // Render the App
        render(<App />);
        // Get the count in the same way the user would, by looking for 'Count'
        let count = screen.getByText(/count:/);
        // As long as the h1 element contains a '0' this test will pass
        expect(count).toContain(0);
        // Once again get the button in the same the user would, by the 'Increment'
        const button = screen.getByText(/increment/);
        // Simulate the click event
        userEvent.click(button);
        // Refetch the count
        count = screen.getByText(/count:/);
        // The 'Count' should no longer contain a '0'
        expect(count).not.toContain(0);
        // The 'Count' should contain a '1'
        expect(count).toContain(1);
      });
      // And so on...
      it('reset count value when "Reset" btn is clicked', () => {});
      it('decrement count value when "Decrement" btn is clicked', () => {});
    });
    

    如果您对这种测试方式感兴趣,请务必查看@testing-library。大约 2 年前,我从 enzyme 切换过来,从那以后就再也没碰过它。

    【讨论】:

    • 谢谢,这是一个很好的答案,但我尝试了使用酶的相同解决方案但也失败了,模拟按钮单击然后记录计数器也打印 0,我真的会迁移到反应测试库,谢谢
    • 为什么在运行此代码时我得到测试失败,并收到预期的消息 1

      counter 1

    • 添加这个library并使用匹配器.toHaveTextContent。见here specifically
    • 或者,使用count.innerHTMLcount.textContent
    【解决方案4】:

    只需要在测试文件中导入 React,例如:

    import * as React from 'react';
    
    

    之后就可以使用mock函数了。

    import * as React from 'react';
    
    :
    :
    it('increment counter correctlry', () => {
        let wrapper = shallow(<Counter/>);
        const setState = jest.fn();
        const useStateSpy = jest.spyOn(React, 'useState');
    
        useStateSpy.mockImplementation((init) => [init, setState]);
         const button = wrapper.find("button")
         button.simulate('click');
         expect(setState).toHaveBeenCalledWith(1);
    })
    

    【讨论】:

    • 感谢您的帖子,威廉。这如何与组件中的多个 useState 一起工作?
    • 最好不要使用多个useState,你应该使用一个中间函数,例如:``` setStateFn = (key, value) => setState((oldState) => ({ .. .oldState, [key]: 值 })); ```
    • 您能否更新您的答案,使其包含在案例中?我认为这会帮助其他人。谢谢你的时间。
    【解决方案5】:

    你应该使用 React.useState() 而不是 useState(),但是还有其他方法... 在 React 中,您可以使用此配置设置 useState 而不使用 React

    // setupTests.js
        const { configure } = require('enzyme')
        const Adapter = require('@wojtekmaj/enzyme-adapter-react-17')
        const { createSerializer } = require('enzyme-to-json')
    
        configure({ adapter: new Adapter() });
        expect.addSnapshotSerializer(createSerializer({
            ignoreDefaultProps: true,
            mode: 'deep',
            noKey: true,
        }));
    
    import React, { useState } from "react";
    
        const Home = () => {
    
            const [count, setCount] = useState(0);
    
            return (
                <section>
    
                    <h3>{count}</h3>
                    <span>
                        <button id="count-up" type="button" onClick={() => setCount(count + 1)}>Count Up</button>
                        <button id="count-down" type="button" onClick={() => setCount(count - 1)}>Count Down</button>
                        <button id="zero-count" type="button" onClick={() => setCount(0)}>Zero</button>
                    </span>
                </section>
            );
    
        }
    
        export default Home;
    

    // index.test.js

        import { mount } from 'enzyme';
        import Home from '../';
        import React, { useState as useStateMock } from 'react';
    
    
        jest.mock('react', () => ({
            ...jest.requireActual('react'),
            useState: jest.fn(),
        }));
    
        describe('<Home />', () => {
            let wrapper;
    
            const setState = jest.fn();
    
            beforeEach(() => {
                useStateMock.mockImplementation(init => [init, setState]);
                wrapper = mount(<Home />);
            });
    
            afterEach(() => {
                jest.clearAllMocks();
            });
    
            describe('Count Up', () => {
                it('calls setCount with count + 1', () => {
                    wrapper.find('#count-up').simulate('click');
                    expect(setState).toHaveBeenCalledWith(1);
                });
            });
    
            describe('Count Down', () => {
                it('calls setCount with count - 1', () => {
                    wrapper.find('#count-down').props().onClick();
                    expect(setState).toHaveBeenCalledWith(-1);
                });
            });
    
            describe('Zero', () => {
                it('calls setCount with 0', () => {
                    wrapper.find('#zero-count').props().onClick();
                    expect(setState).toHaveBeenCalledWith(0);
                });
            });
        });
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-10-09
      • 1970-01-01
      • 2021-08-09
      • 2019-05-08
      • 2019-03-16
      • 2021-01-06
      • 2020-02-08
      相关资源
      最近更新 更多