如何在玩笑中模拟/监视 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,我真的会迁移到反应测试库,谢谢 为什么在运行此代码时我得到测试失败,并收到预期的消息 1counter 1
添加这个library并使用匹配器.toHaveTextContent
。见here specifically
或者,使用count.innerhtml
或count.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 等)做流类型注释?