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

Posted

技术标签:

【中文标题】使用 lodash.debounce 延迟测试组件失败【英文标题】:Testing component with lodash.debounce delay failing 【发布时间】: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); 【参考方案1】:

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

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

这应该可行。

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

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

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

【讨论】:

以上是关于使用 lodash.debounce 延迟测试组件失败的主要内容,如果未能解决你的问题,请参考以下文章

如何正确使用带有 lodash debounce 的 Vue JS 手表

使用 Lodash debounce 和 React useCallback 输入 onChange 事件

Lodash Debounce 和 Apollo Client 使用LazyQuery 去抖动一次

承诺等待后取消 lodash debounce

Lodash debounce 在 React 中不起作用

如何在 TypeScript 中重新导出单个 lodash 模块