【问题标题】:Testing component with lodash.debounce delay failing使用 lodash.debounce 延迟测试组件失败
【发布时间】:2021-04-09 17:45:38
【问题描述】:

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

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) => {
  const debouncedFn = useCallback(
    debounce((...args) => callback(...args), delay),
    [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.advanceTimersByTime(n),n 等于 500,因为 delay={500}
  • 我试过了,但测试一直失败,状态似乎因为某种原因没有改变,这很奇怪,因为在我之前该组件可以工作并且测试是绿色的它们用 DebounceInput 组件包裹。
  • 你用什么库来测试?
  • 仅反应测试渲染
  • 如果您mock lodash/debounce,您的测试是否通过? import debouce from 'lodash/debounce'; // Tell jest to mock this import jest.mock('lodash/debounce'); // Assign the import a new implementation, in this case it's execute the function given to you debouce.mockImplementation(fn =&gt; fn);

标签: reactjs jestjs lodash debounce


【解决方案1】:

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

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

这应该可行。

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

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2023-04-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-05-12
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多