首页 > 解决方案 > Testing component with lodash.debounce delay failing

问题描述

I have a rich text editor input field that I wanted to wrap with a debounced component. Debounced input component looks like this:

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;

I am using DebouncedInput as a wrapper component for MediumEditor:

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

MediumEditor component does some sanitation work that I would like to test, for example stripping html tags:

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;

And this is how I am testing this:

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

When I run this test I get:

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

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

I have also tried running the test with jest.runAllTimers(); before checking the result, but then I get:

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

I have also tried with:

jest.advanceTimersByTime(500);

But the test keeps failing, I get the old state of the text. It seems like the state just doesn't change for some reason, which is weird since the component used to work and the test were green before I had them wrapped with DebounceInput component. The parent component where I have MediumEditor has a method onTextChange that should be called from the DebounceInput component since that is the function that is being passed as the onChange prop to the DebounceInput, but in the test, I can see this method is never reached. In the browser, everything works fine, so I don't know why it is not working in the test?

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

On inspecting further I could see that the correct value is being passed in the test all the way to handleChange in DebouncedInput. So, I suspect, there are some problems with lodash.debounce in this test. I am not sure if I should mock this function or does mock come with jest?

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

This is where I suspect the problem is in the test:

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

I have tried with mocking debounce like this:

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

That gave me error:

TypeError: _lodash.default.mockImplementation is not a function

How should I fix this?

标签: reactjsjestjslodashdebounce

解决方案


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

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

这应该有效。

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

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

组件测试的最佳方法是像您的用户一样模拟组件上的操作,例如,在输入组件中,模拟组件上的更改/输入事件(这是您的用户在实际应用程序中键入时所做的事情)。


推荐阅读