如何在玩笑中模拟/监视 useState 钩子?

Posted

技术标签:

【中文标题】如何在玩笑中模拟/监视 useState 钩子?【英文标题】:How to mock/spy useState hook in jest? 【发布时间】: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

【问题讨论】:

我同意你的问题更简单。但是,只需在 *** 中搜索 'mock usestate' 并查看有关此的许多其他问题。链接问题中您的问题的答案也可能适用。 好吧,有一条关于要点的评论说它可以工作gist.github.com/agiveygives/…你只需要使用React.useState() 评论说使用React.useState()而不是导入 useState from 'react' 另外,你在嘲笑useState,所以你不应该期望它真正更新状态,你只能检查它是否被调用 我真的不知道,让我研究一下原因,我会添加答案,以便以后来的人可以学到新的东西 【参考方案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 的回答让我找到了正确的方向,我想出了这个解决方案:

    从 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';
    稍后在 beforeEach 中,将其设置为原始 useState,以便在所有需要它不被模拟的情况下

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

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

【讨论】:

以上是关于如何在玩笑中模拟/监视 useState 钩子?的主要内容,如果未能解决你的问题,请参考以下文章

如何编写依赖于 React 中 useState 钩子的条件渲染组件的测试?

如何为 React 钩子(useState 等)做流类型注释?

如何模拟反应自定义钩子返回值?

如何在反应中使用带有useState钩子的回调[重复]

如何在 React 中正确使用 useState 钩子和 typescript?

开玩笑地模拟 useDispatch 并在功能组件中使用该调度操作来测试参数