使用 useState() 钩子测试功能组件时设置状态

Posted

技术标签:

【中文标题】使用 useState() 钩子测试功能组件时设置状态【英文标题】:Set state when testing functional component with useState() hook 【发布时间】:2019-08-15 22:52:11 【问题描述】:

当我用酶测试类组件时,我可以使用wrapper.setState() 来设置状态。当我使用useState() 钩子测试功能组件时,我现在该怎么做?

例如在我的组件中:

const [mode, setMode] = useState("my value");

我想在我的测试中更改mode

【问题讨论】:

【参考方案1】:

在测试文件的顶部,可以先定义为:

  import  useState  from 'react';

  jest.mock('react', () => (
    ...jest.requireActual('react'),
    useState: jest.fn()
  ));

  const useStateMock: jest.Mock<typeof useState> = useState as never;

之后在每个测试中可以使用不同的值来测试:

  const setValue = jest.fn();
  useStateMock
    .mockImplementation(() => ['value', setValue]);

【讨论】:

【参考方案2】:

你应该这样使用

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

【讨论】:

【参考方案3】:

这是我发现的方法,我并不是说这是对还是错。在我的例子中,一段代码依赖于被设置为特定值的状态。我会保留我对 React 测试的意见。

在您的测试文件中: 调整你对 react 库的导入

import * as React from 'react'

然后在你的测试中监视 useState 并模拟它的实现

const stateSetter = jest.fn()
jest
.spyOn(React, 'useState')
//Simulate that mode state value was set to 'new mode value'
.mockImplementation(stateValue => [stateValue='new mode value', stateSetter])

请注意,模拟 useState 这将适用于为您的测试调用 useState 的所有实例,因此如果您正在查看多个状态值,它们将全部设置为“新模式值” .其他人也许可以帮助您解决这个问题。希望对您有所帮助。

【讨论】:

2 个 useState 的情况如何? 在有 2 个或更多 useStates 的情况下,我们使用了 mockImplementationOnce 并取得了一些成功,但我会警告你它并不漂亮。【参考方案4】:

当使用来自钩子的状态时,您的测试必须忽略状态等实现细节才能正确测试它。 您仍然可以确保组件将正确的状态传递给其子级。

您可以在由 Kent C. Dodds 撰写的 blog post 中找到一个很好的示例。

这里是摘录的代码示例。

依赖于状态实现细节的测试 -

test('setOpenIndex sets the open index state properly', () => 
  const wrapper = mount(<Accordion items=[] />)
  expect(wrapper.state('openIndex')).toBe(0)
  wrapper.instance().setOpenIndex(1)
  expect(wrapper.state('openIndex')).toBe(1)
)

不依赖于状态实现细节的测试 -

test('counter increments the count', () => 
  const container = render(<Counter />)
  const button = container.firstChild
  expect(button.textContent).toBe('0')
  fireEvent.click(button)
  expect(button.textContent).toBe('1')
)

【讨论】:

哦.. 所以当我们使用钩子时我们无法测试状态 目前不直接。我很难想象这是怎么可能的,因为钩子的语法依赖于调用顺序而不是命名。我把它看成是无法访问私有的测试类——拥有访问权限很好,但这通常意味着你可以写得更好:) 嗯,我想测试确实不应该依赖状态变化,而是应该测试外观 所以我可以说著名的 Kent C. Dodds 真的错了。当大量依赖只知道如何发推文的程序员的话时,就会发生这种情况。我为 IBM 工作,我们有义务测试这些钩子。显然,我们必须测试挂钩的功能,我们不能忽视它们存在且至关重要的事实。我遵循了这篇帖子blog.carbonfive.com/2019/08/05/… 的一些指导,而且我很快就会在这里发布和回答,让你知道如何真正测试钩子,这不像肯特说的那样。他错了。 第二个测试和E2E测试一样好,它不是单元测试,消费者是开发者而不是浏览器

以上是关于使用 useState() 钩子测试功能组件时设置状态的主要内容,如果未能解决你的问题,请参考以下文章

反应钩子 useState 不使用 onSubmit 更新

如何使用 useState - 钩子将类组件(输入,条件)更改为功能组件?

在 useState 钩子中反应设置状态

使用钩子时 Jest/Enzyme 测试抛出错误

React Native Jest - 如何使用多个钩子测试功能组件?无法存根 AccessiblityInfo 模块

React useState钩子无限期运行 - 无限循环