我正在尝试为去抖动功能编写一个单元测试。我很难想它。
这是代码:
function debouncer(func, wait, immediate) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => {
timeout = null;
if (!immediate)
func.apply(this, args);
}, wait);
if (immediate && !timeout)
func.apply(this, args);
};
}
我应该如何开始?
实际上,您不需要使用 Sinon 来测试去抖动。Jest 可以模拟 JavaScript 代码中的所有计时器。
查看以下代码(它是 TypeScript,但您可以轻松地将其转换为 JavaScript(:
import * as _ from 'lodash';
// Tell Jest to mock all timeout functions
jest.useFakeTimers();
describe('debounce', () => {
let func: jest.Mock;
let debouncedFunc: Function;
beforeEach(() => {
func = jest.fn();
debouncedFunc = _.debounce(func, 1000);
});
test('execute just once', () => {
for (let i = 0; i < 100; i++) {
debouncedFunc();
}
// Fast-forward time
jest.runAllTimers();
expect(func).toBeCalledTimes(1);
});
});
详细信息:计时器模拟
如果你在代码中这样做:
import debounce from 'lodash/debounce';
myFunc = debounce(myFunc, 300);
并且你想测试函数myFunc
或调用它的函数,那么在你的测试中,你可以使用jest
来模拟debounce
的实现,让它只返回你的函数:
import debounce from 'lodash/debounce';
// Tell Jest to mock this import
jest.mock('lodash/debounce');
it('my test', () => {
// ...
debounce.mockImplementation(fn => fn); // Assign the import a new implementation. In this case it's to execute the function given to you
// ...
});
来源: https://gist.github.com/apieceofbart/d28690d52c46848c39d904ce8968bb27
您可能需要检查去抖函数中的逻辑:
timeout
将始终由最后一个if()
语句设置this
将始终undefined
,因为箭头函数使用"封闭词法上下文的this
值",并且debouncer()
被设计为用作独立函数。
话虽如此,听起来您的真正问题是关于测试去抖函数。
测试去抖函数
您可以通过使用模拟来跟踪函数调用和假计时器来模拟时间的流逝来测试函数是否被取消反弹。
下面是一个简单的示例,使用Jest
模拟函数和Sinon
使用Lodash
debounce()
去抖动的函数的假计时器:
const _ = require('lodash');
import * as sinon from 'sinon';
let clock;
beforeEach(() => {
clock = sinon.useFakeTimers();
});
afterEach(() => {
clock.restore();
});
test('debounce', () => {
const func = jest.fn();
const debouncedFunc = _.debounce(func, 1000);
// Call it immediately
debouncedFunc();
expect(func).toHaveBeenCalledTimes(0); // func not called
// Call it several times with 500ms between each call
for(let i = 0; i < 10; i++) {
clock.tick(500);
debouncedFunc();
}
expect(func).toHaveBeenCalledTimes(0); // func not called
// wait 1000ms
clock.tick(1000);
expect(func).toHaveBeenCalledTimes(1); // func called
});
我喜欢这个类似的版本更容易失败:
jest.useFakeTimers();
test('execute just once', () => {
const func = jest.fn();
const debouncedFunc = debounce(func, 500);
// Execute for the first time
debouncedFunc();
// Move on the timer
jest.advanceTimersByTime(250);
// try to execute a 2nd time
debouncedFunc();
// Fast-forward time
jest.runAllTimers();
expect(func).toBeCalledTimes(1);
});
使用现代假计时器(Jest 27 已经默认(,您可以更简洁地测试它:
import debounce from "lodash.debounce";
describe("debounce", () => {
beforeEach(() => {
jest.useFakeTimers("modern");
});
afterEach(() => {
jest.useRealTimers();
});
it("should work properly", () => {
const callback = jest.fn();
const debounced = debounce(callback, 500);
debounced();
expect(callback).not.toBeCalled();
jest.advanceTimersByTime(100);
debounced();
expect(callback).not.toBeCalled();
jest.advanceTimersByTime(499);
expect(callback).not.toBeCalled();
jest.advanceTimersByTime(1);
expect(callback).toBeCalledTimes(1);
});
it("should fire with lead", () => {
const callback = jest.fn();
const debounced = debounce(callback, 500, { leading: true });
expect(callback).not.toBeCalled();
debounced();
expect(callback).toBeCalledTimes(1);
jest.advanceTimersByTime(100);
debounced();
expect(callback).toBeCalledTimes(1);
jest.advanceTimersByTime(499);
expect(callback).toBeCalledTimes(1);
jest.advanceTimersByTime(1);
expect(callback).toBeCalledTimes(2);
});
});
你可以把它实现为一个状态钩子,像这样被反弹......
import debounce from "lodash.debounce";
import { Dispatch, useCallback, useState } from "react";
export function useDebouncedState<S>(
initialValue: S,
wait: number,
debounceSettings?: Parameters<typeof debounce>[2]
): [S, Dispatch<S>] {
const [state, setState] = useState<S>(initialValue);
const debouncedSetState = useCallback(
debounce(setState, wait, debounceSettings),
[wait, debounceSettings]
);
return [state, debouncedSetState];
}
并测试为
/**
* @jest-environment jsdom
*/
import { act, render, waitFor } from '@testing-library/react';
import React from 'react';
import { useDebouncedState } from "./useDebouncedState";
describe("useDebounceState", () => {
beforeEach(() => {
jest.useFakeTimers("modern");
});
afterEach(() => {
jest.useRealTimers();
});
it("should work properly", async () => {
const callback = jest.fn();
let clickCount = 0;
function MyComponent() {
const [foo, setFoo] = useDebouncedState("bar", 500);
callback();
return <div data-testid="elem" onClick={() => { ++clickCount; setFoo("click " + clickCount); }}>{foo}</div>
}
const { getByTestId } = render(<MyComponent />)
const elem = getByTestId("elem");
expect(callback).toBeCalledTimes(1);
expect(elem.textContent).toEqual("bar");
jest.advanceTimersByTime(100);
elem.click();
expect(callback).toBeCalledTimes(1);
expect(elem.textContent).toEqual("bar");
jest.advanceTimersByTime(399);
expect(callback).toBeCalledTimes(1);
expect(elem.textContent).toEqual("bar");
act(() => jest.advanceTimersByTime(1));
await waitFor(() => {
expect(callback).toBeCalledTimes(2);
expect(elem.textContent).toEqual("click 1");
});
elem.click();
await waitFor(() => {
expect(callback).toBeCalledTimes(2);
expect(elem.textContent).toEqual("click 1");
});
act(() => jest.advanceTimersByTime(500));
await waitFor(() => {
expect(callback).toBeCalledTimes(3);
expect(elem.textContent).toEqual("click 2");
});
});
});
源代码可在 https://github.com/trajano/react-hooks-tests/tree/master/src/useDebouncedState
花了很多时间来弄清楚...终于奏效了..
jest.mock('lodash', () => {
const module = jest.requireActual('lodash');
module.debounce = jest.fn(fn => fn);
return module;
});
这是我的 3 个基本测试:
测试去抖逻辑有基础。
请注意,所有测试都是异步的,因为测试的内容的性质本身就是异步的。
import debounce from 'lodash.debounce'
const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
test('called repeatedly', async () => {
const DELAY = 100;
let callCount = 0;
const debounced = debounce(() => ++callCount, DELAY)
for( let i = 4; i--; )
debounced()
await delay(DELAY)
expect( callCount ).toBe(1)
})
test('called repeatedly exactly after the delay', async () => {
const DELAY = 100;
let callCount = 0, times = 3;
const debounced = debounce(() => ++callCount, DELAY)
for( let i = times; i--; ) {
debounced()
await delay(DELAY)
}
await delay(DELAY * times)
expect( callCount ).toBe(3)
})
test('called repeatedly at an interval small than the delay', async () => {
const DELAY = 100;
let callCount = 0, times = 6;
const debounced = debounce(() => ++callCount, DELAY)
for( let i = times; i--; ) {
debounced()
await delay(DELAY/2)
}
await delay(DELAY * times)
expect( callCount ).toBe(1)
})
这些测试是我编写的,而不是取自 lodash去抖动测试源代码
另一种方法是刷新去抖动函数以使其立即执行:
test('execute just once', () => {
const func = jest.fn();
const debouncedFunc = debounce(func, 500);
// Execute for the first time
debouncedFunc();
debouncedFunc.flush();
// try to execute a 2nd time
debouncedFunc();
debouncedFunc.flush();
expect(func).toBeCalledTimes(1);
});