发布于 2026-01-06 11 阅读
0

React Hooks:使用 Enzyme 测试自定义 Hooks 你将学到什么

React Hooks:使用 Enzyme 测试自定义 Hooks

你将学到什么

TL;DR - 将你的自定义钩子封装在一个组件中,并进行浅渲染以测试实现细节。

你将学到什么

  • React 测试策略
    • 用户可观察行为
    • 实施细节
  • 使用 Enzyme 测试自定义钩子

测试策略

测试 React 代码库大致有两种策略。

  1. 测试用户可观察行为
  2. 测试实施细节

测试用户可观察行为

测试用户可观察行为意味着编写针对组件的测试。

  • 组件的渲染方式
  • 当用户与 DOM 交互时,组件如何重新渲染。
  • props/state 如何控制渲染内容

请考虑以下组件 -Greet

function Greet({ user = 'User' }) {
  const [name, setName] = React.useState(user);

  return <div onClick={() => setName('Pinocchio')}>Hello, {name}!</div>;
}
Enter fullscreen mode Exit fullscreen mode

测试用户可观察行为Greet意味着

  • 测试Greet渲染是否流畅不崩溃。
  • 测试Hello, User!当未传递用户属性时是否渲染。
  • 测试当将值传递给prop时是否Hello, Bruce!渲染成功Bruceuser
  • Hello, Pinocchio!测试当用户点击元素时文本是否会发生变化。

测试实施细节

测试实现细节意味着编写针对状态逻辑的测试。

  • 如何使用默认值/属性值初始化状态
  • 当处理程序被调用时,状态如何变化?

考虑同一个组件——Greet

function Greet({ user = 'User' }) {
  const [name, setName] = React.useState(user);

  return <div onClick={() => setName('Pinocchio')}>Hello, {name}!</div>;
}
Enter fullscreen mode Exit fullscreen mode

测试实现细节Greet意味着

  • 当未向用户传递属性时,测试是否name设置为默认值UserGreet
  • name当用户属性传递给某个对象时,测试是否将其设置为属性值。Greet
  • 测试调用name时是否已更新。setName

使用 Enzyme 测试自定义钩子

注意:请确保您的 React 版本为 10.1 ^16.8.5。之前的版本中,Hooks 无法通过 Enzyme 的浅渲染重新渲染组件,React 团队已在此版本中修复了此问题。如果您的 React 版本低于 10.1,您可能需要.update()在每次更改后使用 Enzyme 的 mount 和您的包装器来测试重新渲染。

当你在为包含展示(UI)逻辑并向 DOM 渲染元素的组件编写测试时,测试实现细节似乎没有必要,甚至可能被认为是一种不好的做法。但是,自定义 Hook只包含状态逻辑,因此我们必须彻底测试其实现细节,以便准确了解自定义 Hook 在组件中的行为。

让我们编写一个自定义钩子来更新和验证表单字段。

/* useFormField.js */

import React from 'react';

function useFormField(initialVal = '') {
  const [val, setVal] = React.useState(initialVal);
  const [isValid, setValid] = React.useState(true);

  function onChange(e) {
    setVal(e.target.value);

    if (!e.target.value) {
      setValid(false);
    } else if (!isValid) setValid(true);
  }

  return [val, onChange, isValid];
}

export default useFormField;
Enter fullscreen mode Exit fullscreen mode

自定义 Hook 功能虽然强大,能够抽象出代码中可重用的逻辑,但它们也存在一个局限性。尽管它们只是 JavaScript 函数,却只能在 React 组件内部运行。你不能直接调用它们并针对 Hook 的返回值编写测试。你必须将它们封装在一个 React 组件中,然后测试组件返回的值。

  • 自定义钩子函数无法像 JavaScript 函数那样进行测试。
  • 自定义 Hook 应该封装在 React 组件中以测试其行为

由于 Hook 具有可组合性,我们可以将 Hook 作为 prop 传递给组件,一切都会按预期运行。我们还可以编写一个包装组件来渲染和测试我们的 Hook。

/* useFormField.test.js */

function HookWrapper(props) {
  const hook = props.hook ? props.hook() : undefined;
  return <div hook={hook} />;
}
Enter fullscreen mode Exit fullscreen mode

现在我们可以像访问 JavaScript 对象一样访问这个钩子,并测试它的行为。

/* useFormField.test.js */

import React from 'react';
import { shallow } from 'enzyme';
import useFormField from './useFormField';

function HookWrapper(props) {
  const hook = props.hook ? props.hook() : undefined;
  return <div hook={hook} />;
}

it('should set init value', () => {
  let wrapper = shallow(<HookWrapper hook={() => useFormField('')} />);

  let { hook } = wrapper.find('div').props();
  let [val, onChange, isValid] = hook;
  expect(val).toEqual('');

  wrapper = shallow(<HookWrapper hook={() => useFormField('marco')} />);

  // destructuring objects - {} should be inside brackets - () to avoid syntax error
  ({ hook } = wrapper.find('div').props());
  [val, onChange, isValid] = hook;
  expect(val).toEqual('marco');
});
Enter fullscreen mode Exit fullscreen mode

自定义钩子的完整测试套件useFormField如下所示。

/* useFormField.test.js */

import React from 'react';
import { shallow } from 'enzyme';
import useFormField from './useFormField';

function HookWrapper(props) {
  const hook = props.hook ? props.hook() : undefined;
  return <div hook={hook} />;
}

describe('useFormField', () => {
  it('should render', () => {
    let wrapper = shallow(<HookWrapper />);

    expect(wrapper.exists()).toBeTruthy();
  });

  it('should set init value', () => {
    let wrapper = shallow(<HookWrapper hook={() => useFormField('')} />);

    let { hook } = wrapper.find('div').props();
    let [val, onChange, isValid] = hook;
    expect(val).toEqual('');

    wrapper = shallow(<HookWrapper hook={() => useFormField('marco')} />);

    // destructuring objects - {} should be inside brackets - () to avoid syntax error
    ({ hook } = wrapper.find('div').props());
    [val, onChange, isValid] = hook;
    expect(val).toEqual('marco');
  });

  it('should set the right val value', () => {
    let wrapper = shallow(<HookWrapper hook={() => useFormField('marco')} />);

    let { hook } = wrapper.find('div').props();
    let [val, onChange, isValid] = hook;
    expect(val).toEqual('marco');

    onChange({ target: { value: 'polo' } });

    ({ hook } = wrapper.find('div').props());
    [val, onChange, isValid] = hook;
    expect(val).toEqual('polo');
  });

  it('should set the right isValid value', () => {
    let wrapper = shallow(<HookWrapper hook={() => useFormField('marco')} />);

    let { hook } = wrapper.find('div').props();
    let [val, onChange, isValid] = hook;
    expect(val).toEqual('marco');
    expect(isValid).toEqual(true);

    onChange({ target: { value: 'polo' } });

    ({ hook } = wrapper.find('div').props());
    [val, onChange, isValid] = hook;
    expect(val).toEqual('polo');
    expect(isValid).toEqual(true);

    onChange({ target: { value: '' } });

    ({ hook } = wrapper.find('div').props());
    [val, onChange, isValid] = hook;
    expect(val).toEqual('');
    expect(isValid).toEqual(false);
  });
});
Enter fullscreen mode Exit fullscreen mode

渲染自定义钩子并将其作为属性访问,应该可以让我们完全访问其返回值。

如果你在自定义 hook 中使用useEffecthook,请确保用 ` ReactTestUtils.act()shallow ` 包裹或mount调用 `request` ,以便在断言之前清除 effects。Enzyme 可能很快会在内部支持此功能,但目前这是必需的。更多信息请参见hooks-faq

act(() => {
  wrapper = shallow(<HookWrapper />);
});
Enter fullscreen mode Exit fullscreen mode

本文中的所有代码片段都可以在仓库testing-hooks中找到,其中包含一个可运行的示例。

测试顺利!🎉

文章来源:https://dev.to/flexdinesh/react-hooks-test-custom-hooks-with-enzyme-40ib