使用 lodash.debounce 延迟测试组件失败

问题描述

我有一个富文本编辑器输入字段,我想用去抖动组件包装它。去抖动输入组件如下所示:

import { useState,useCallback } from 'react';
import debounce from 'lodash.debounce';

const useDebounce = (callback,delay) => {
  const debouncedFn = useCallback(
    debounce((...args) => callback(...args),delay),[delay] // will recreate if delay changes
  );
  return debouncedFn;
};

function DebouncedInput(props) {
  const [value,setValue] = useState(props.value);
  const debouncedSave = useDebounce((nextValue) => props.onChange(nextValue),props.delay);

  const handleChange = (nextValue) => {
    setValue(nextValue);
    debouncedSave(nextValue);
  };

  return props.renderProps({ onChange: handleChange,value });
}

export default DebouncedInput;

我使用 DebouncedInput 作为 MediumEditor 的包装组件:

<DebouncedInput
  value={task.text}
  onChange={(text) => onTextChange(text)}
  delay={500}
  renderProps={(props) => (
    <MediumEditor
      {...props}
      id="task"
      style={{ height: '100%' }}
      placeholder="Task text…"
      disabled={readOnly}
      key={task.id}
    />
  )}
/>;

MediumEditor 组件做了一些我想测试的清理工作,例如剥离 html 标签

class MediumEditor extends React.Component {
  static props = {
    id: PropTypes.string,value: PropTypes.string,onChange: PropTypes.func,disabled: PropTypes.bool,uniqueID: PropTypes.any,placeholder: PropTypes.string,style: PropTypes.object,};

  onChange(text) {
    this.props.onChange(stripHtml(text) === '' ? '' : fixExcelPaste(text));
  }

  render() {
    const {
      id,value,onChange,disabled,placeholder,style,uniqueID,...restProps
    } = this.props;
    return (
      <div style={{ position: 'relative',height: '100%' }} {...restProps}>
        {disabled && (
          <div
            style={{
              position: 'absolute',width: '100%',height: '100%',cursor: 'not-allowed',zIndex: 1,}}
          />
        )}
        <Editor
          id={id}
          data-testid="medium-editor"
          options={{
            toolbar: {
              buttons: ['bold','italic','underline','subscript','superscript'],},spellcheck: false,disableEditing: disabled,placeholder: { text: placeholder || 'Skriv inn tekst...' },}}
          onChange={(text) => this.onChange(text)}
          text={value}
          style={{
            ...style,background: disabled ? 'transparent' : 'white',borderColor: disabled ? 'grey' : '#FF9600',overflowY: 'auto',color: '#444F55',}}
        />
      </div>
    );
  }
}

export default MediumEditor;

这就是我测试的方式:

it('not stripping html tags if there is text',async () => {
  expect(editor.instance.state.text).toEqual('Lorem ipsum ...?');
  const mediumEditor = editor.findByProps({ 'data-testid': 'medium-editor' });
  const newText = '<p><b>New text,Flesk</b></p>';
  mediumEditor.props.onChange(newText);
  // jest.runAllTimers();
  expect(editor.instance.state.text).toEqual(newText);
});

当我运行这个测试时,我得到:

Error: expect(received).toEqual(expected) // deep equality

Expected: "<p><b>New text,Flesk</b></p>"
Received: "Lorem ipsum ...?"

在检查结果之前,我也尝试使用 jest.runAllTimers(); 运行测试,但我得到:

Error: Ran 100000 timers,and there are still more! Assuming we've hit an infinite recursion and bailing out...

我也试过:

jest.advanceTimersByTime(500);

但是测试一直失败,我得到了文本的旧状态。 状态似乎由于某种原因没有改变,这很奇怪,因为在我用 DebounceInput 组件包裹它们之前,组件曾经可以工作并且测试是绿色的。 我有 MediumEditor 的父组件有一个方法 onTextChange 应该从 DebounceInput 组件调用,因为这是作为传递的函数DebounceInputonChange 道具,但在测试中,我可以看到从未达到此方法。在浏览器中,一切正常,所以我不知道为什么它在测试中不起作用?

onTextChange(text) {
  console.log('text',text);
  this.setState((state) => {
    return {
      task: { ...state.task,text },isDirty: true,};
  });
}

进一步检查时,我可以看到正确的值在测试中一直传递到 DebouncedInput 中的 handleChange。所以,我怀疑在这个测试中 lodash.debounce 存在一些问题。我不确定我是应该模拟这个功能还是模拟带有玩笑?

const handleChange = (nextValue) => {
  console.log(nextValue);
  setValue(nextValue);
  debouncedSave(nextValue);
};

这是我怀疑问题出在测试中的地方:

const useDebounce = (callback,[delay] // will recreate if delay changes
  );
  return debouncedFn;
};

我尝试过像这样模拟去抖动:

import debounce from 'lodash.debounce'
jest.mock('lodash.debounce');
debounce.mockImplementation(() => jest.fn(fn => fn));

这给了我错误

TypeError: _lodash.default.mockImplementation is not a function

我应该如何解决这个问题?

解决方法

我猜您正在使用酶(来自道具访问)。 为了测试一些依赖于 jest 中的计时器的代码:

  1. 标记为玩笑使用假定时器调用 jest.useFakeTimers()
  2. 渲染你的组件
  3. 进行更改(这将启动计时器,在您的情况下是状态更改),注意当您从酶更改状态时,您需要调用 componentWrapper.update()
  4. 使用 jest.runOnlyPendingTimers()
  5. 提前计时器

这应该有效。

关于测试 React 组件的一些旁注:

  1. 如果您想测试 onChange 的功能,请测试直接组件(在您的情况下为 MediumEditor),没有必要测试整个包装组件 来测试 onChange 功能
  2. 不要从测试中更新状态,它会使您的测试与特定实现高度耦合,证明,重命名状态变量名称,您的组件的功能不会改变,但您的测试会失败,因为它们会尝试更新不存在状态变量的状态。
  3. 不要在测试中调用 onChange 道具(或任何其他道具)。它使您的测试更加了解实现(=高耦合与组件实现),实际上它们不会检查您的组件是否正常工作,例如认为由于某种原因您没有将 onChange 道具传递给输入,您的测试将通过(因为您的测试正在调用 onChange 道具),但实际上它不起作用。

组件测试的最佳方法是模拟用户会在组件上执行的操作,例如,在输入组件中,在组件上模拟更改/输入事件(这是您的用户在实际应用中执行的操作)类型)。

,

为什么不直接使用自己的去抖动实现?这很容易实现。在 React 中,它是:

export function useDebouncedValue(state,delay) {
  const [debouncedValue,setDebouncedValue] = useState(state);

  useEffect(() => {
    const timeout = setTimeout(() => {
      setDebouncedValue(state);
    },delay);
    return () => {
      clearTimeout(timeout);
    };
  },[state,delay]);

  return debouncedValue;
}

你可以这样做:

// Value saved immediatelly
const [value,setValue] = useState('');      // Value with 1 second delay
const debouncedValue = useDebouncedValue(value,1000);