测试组件 lodash.debounce 延迟失败

Lef*_*eff 5 debouncing lodash reactjs jestjs

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

\n
import { useState, useCallback } from \'react\';\nimport debounce from \'lodash.debounce\';\n\nconst useDebounce = (callback, delay) => {\n  const debouncedFn = useCallback(\n    debounce((...args) => callback(...args), delay),\n    [delay] // will recreate if delay changes\n  );\n  return debouncedFn;\n};\n\nfunction DebouncedInput(props) {\n  const [value, setValue] = useState(props.value);\n  const debouncedSave = useDebounce((nextValue) => props.onChange(nextValue), props.delay);\n\n  const handleChange = (nextValue) => {\n    setValue(nextValue);\n    debouncedSave(nextValue);\n  };\n\n  return props.renderProps({ onChange: handleChange, value });\n}\n\nexport default DebouncedInput;\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n
<DebouncedInput\n  value={task.text}\n  onChange={(text) => onTextChange(text)}\n  delay={500}\n  renderProps={(props) => (\n    <MediumEditor\n      {...props}\n      id="task"\n      style={{ height: \'100%\' }}\n      placeholder="Task text\xe2\x80\xa6"\n      disabled={readOnly}\n      key={task.id}\n    />\n  )}\n/>;\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n
class MediumEditor extends React.Component {\n  static props = {\n    id: PropTypes.string,\n    value: PropTypes.string,\n    onChange: PropTypes.func,\n    disabled: PropTypes.bool,\n    uniqueID: PropTypes.any,\n    placeholder: PropTypes.string,\n    style: PropTypes.object,\n  };\n\n  onChange(text) {\n    this.props.onChange(stripHtml(text) === \'\' ? \'\' : fixExcelPaste(text));\n  }\n\n  render() {\n    const {\n      id,\n      value,\n      onChange,\n      disabled,\n      placeholder,\n      style,\n      uniqueID,\n      ...restProps\n    } = this.props;\n    return (\n      <div style={{ position: \'relative\', height: \'100%\' }} {...restProps}>\n        {disabled && (\n          <div\n            style={{\n              position: \'absolute\',\n              width: \'100%\',\n              height: \'100%\',\n              cursor: \'not-allowed\',\n              zIndex: 1,\n            }}\n          />\n        )}\n        <Editor\n          id={id}\n          data-testid="medium-editor"\n          options={{\n            toolbar: {\n              buttons: [\'bold\', \'italic\', \'underline\', \'subscript\', \'superscript\'],\n            },\n            spellcheck: false,\n            disableEditing: disabled,\n            placeholder: { text: placeholder || \'Skriv inn tekst...\' },\n          }}\n          onChange={(text) => this.onChange(text)}\n          text={value}\n          style={{\n            ...style,\n            background: disabled ? \'transparent\' : \'white\',\n            borderColor: disabled ? \'grey\' : \'#FF9600\',\n            overflowY: \'auto\',\n            color: \'#444F55\',\n          }}\n        />\n      </div>\n    );\n  }\n}\n\nexport default MediumEditor;\n
Run Code Online (Sandbox Code Playgroud)\n

这就是我测试的方式:

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

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

\n
Error: expect(received).toEqual(expected) // deep equality\n\nExpected: "<p><b>New text, Flesk</b></p>"\nReceived: "Lorem ipsum ...?"\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n
Error: Ran 100000 timers, and there are still more! Assuming we\'ve hit an infinite recursion and bailing out...\n
Run Code Online (Sandbox Code Playgroud)\n

我也尝试过:

\n
Error: expect(received).toEqual(expected) // deep equality\n\nExpected: "<p><b>New text, Flesk</b></p>"\nReceived: "Lorem ipsum ...?"\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n
Error: Ran 100000 timers, and there are still more! Assuming we\'ve hit an infinite recursion and bailing out...\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n
jest.advanceTimersByTime(500);\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n
onTextChange(text) {\n  console.log(\'text\', text);\n  this.setState((state) => {\n    return {\n      task: { ...state.task, text },\n      isDirty: true,\n    };\n  });\n}\n
Run Code Online (Sandbox Code Playgroud)\n

我尝试过像这样嘲笑反跳:

\n
const handleChange = (nextValue) => {\n  console.log(nextValue);\n  setValue(nextValue);\n  debouncedSave(nextValue);\n};\n
Run Code Online (Sandbox Code Playgroud)\n

这给了我错误:

\n
\n
TypeError: _lodash.default.mockImplementation is not a function\n
Run Code Online (Sandbox Code Playgroud)\n
\n

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

\n

fel*_*osh 2

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

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

这应该有效。

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

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

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