简体   繁体   English

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

[英]How to mock/spy useState hook in jest?

I am trying to spy on useState React hook but i always get the test failed我试图监视 useState React 钩子,但我总是测试失败

This is my React component:这是我的 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 : 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);
})

Unfortunately this doesn't work and i get the test failed with that message:不幸的是,这不起作用,我得到该消息的测试失败:

expected 1
Number of calls: 0

You need to use React.useState instead of the single import useState .您需要使用React.useState而不是单个导入useState

I think is about how the code gets transpiled , as you can see in the babel repl the useState from the single import ends up being different from the one of the module import我认为是关于代码如何转换的,正如您在 babel repl 中看到的useState来自单个导入的useState最终与模块导入之一不同

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

So you spy on _react.default.useState but your component uses _react.useState .所以你监视_react.default.useState但你的组件使用_react.useState It seems impossible to spyOn a single import since you need the function to belong to an object, here is a very extensive guide that explains the ways of mocking/spying modules https://github.com/HugoDF/mock-spy-module-import监视单个导入似乎是不可能的,因为您需要该函数属于一个对象,这里有一个非常广泛的指南,解释了模拟/监视模块的方法https://github.com/HugoDF/mock-spy-module-进口

And as @Alex Mackay mentioned, you probably want to change your mindset about testing react components, moving to react-testing-library is recommended, but if you really need to stick to enzyme you don't need to go that far as to mock react library itself正如@Alex Mackay 提到的,您可能想改变测试 React 组件的思维方式,建议使用 react-testing-library,但如果您真的需要坚持使用酶,则无需进行模拟反应库本身

diedu's answer led me the right direction and I came up with this solution: dieu 的回答让我找到了正确的方向,我想出了这个解决方案:

  1. Mock use state from react to return a jest.fn() as useState: 1.1 Also import useState immediately after - that will now be e jest mock (returned from the jest.fn() call) Mock use state from react 返回 jest.fn() as useState: 1.1 也立即导入 useState - 现在将是 e jest mock(从 jest.fn() 调用返回)

 jest.mock('react', ()=>({ ...jest.requireActual('react'), useState: jest.fn() })) import { useState } from 'react';

  1. Later on in the beforeEach, set it to the original useState, for all the cases where you need it to not be mocked稍后在 beforeEach 中,将其设置为原始 useState,对于所有需要它不被嘲笑的情况

 describe("Test", ()=>{ beforeEach(()=>{ useState.mockImplementation(jest.requireActual('react').useState); //other preperations }) //tests })

  1. In the test itself mock it as needed:在测试本身中根据需要模拟它:

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

Parting notes: While it might be conceptually somewhat wrong to get your hands dirty inside the "black box" one is unit testing, it is indeed super useful at times to do it.离别笔记:虽然在“黑匣子”是单元测试中弄脏手在概念上可能有些错误,但有时这样做确实非常有用。

Annoyingly Codesandbox is currently having trouble with its testing module so I can't post a working example but I will try to explain why mocking useState is generally a bad thing to do.恼人的是 Codesandbox 目前在其测试模块上遇到问题,所以我无法发布一个工作示例,但我会尝试解释为什么useState通常是一件坏事。

The user doesn't care if useState has been called, they care about when I click increment the count should increase by one therefore that is what you should be testing for.用户并不关心useState是否已被调用,他们关心的是当我单击 increment 时,计数应该增加 1,因此这就是您应该测试的内容。

// 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', () => {});
});

Definitely check out @testing-library if you are interested in this style of testing.如果您对这种测试方式感兴趣,请务必查看@testing-library I switched from enzyme about 2 years ago and haven't touched it since.大约 2 年前我从enzyme转过来,从那以后就没有接触过它。

just you need to import React in your test file like:只需要在测试文件中导入 React,例如:

import * as React from 'react';

after that you can use the mock function.之后,您可以使用模拟功能。

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

you should use React.useState() instead useState(), But there are other ways... in React you can set useState without React with this config你应该使用 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 // 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);
            });
        });
    });

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM