问题描述
我有一个富文本编辑器输入字段,我想用去抖动组件包装它。去抖动输入组件如下所示:
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 组件调用,因为这是作为传递的函数DebounceInput 的 onChange
道具,但在测试中,我可以看到从未达到此方法。在浏览器中,一切正常,所以我不知道为什么它在测试中不起作用?
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
中的计时器的代码:
- 标记为玩笑使用假定时器调用
jest.useFakeTimers()
- 渲染你的组件
- 进行更改(这将启动计时器,在您的情况下是状态更改),注意当您从酶更改状态时,您需要调用
componentWrapper.update()
- 使用
jest.runOnlyPendingTimers()
提前计时器
这应该有效。
关于测试 React 组件的一些旁注:
- 如果您想测试
onChange
的功能,请测试直接组件(在您的情况下为MediumEditor
),没有必要测试整个包装组件 来测试 onChange 功能 - 不要从测试中更新状态,它会使您的测试与特定实现高度耦合,证明,重命名状态变量名称,您的组件的功能不会改变,但您的测试会失败,因为它们会尝试更新不存在状态变量的状态。
- 不要在测试中调用
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);