【问题标题】:Jest unit test for a debounce function去抖动功能的 Jest 单元测试
【发布时间】:2019-02-12 22:04:48
【问题描述】:

我正在尝试为 debounce 函数编写单元测试。我很难考虑。

这是代码:

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

我应该如何开始?

【问题讨论】:

标签: javascript unit-testing jestjs debounce


【解决方案1】:

您可能需要检查 debouncer 函数中的逻辑:

话虽如此,听起来您真正的问题是关于测试去抖动功能。

测试去抖函数

您可以通过使用模拟跟踪函数调用和假计时器来模拟时间的流逝来测试函数是否已消除抖动。

这是一个使用Jest Mock FunctionSinon fake timers 的简单示例,该函数使用debounce() from Lodash 去抖动:

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

【讨论】:

  • @RecipeCreator 欢迎来到 SO!由于您是新手,如果答案提供了您需要的信息,友好提醒您标记为已完成并投票(当您获得该能力时)
  • 有没有办法在没有 sinon 的情况下完成它?使用 Jest Timers Mocks (jestjs.io/docs/en/timer-mocks)?
  • @BrianAdams 很棒的解决方案!非常容易理解。
【解决方案2】:

其实,你不需要使用 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);
    });
});

更多信息:Timer Mocks

【讨论】:

  • 这很好,但如果你没有使用 jest v27 并遇到无限递归错误,请参阅:stackoverflow.com/a/64336022/4844024
  • jest.useFakeTimers("modern") const foo = jest.fn() test("timer", () => { setTimeout(() => foo(), 2000) jest.runAllTimers () expect(foo).toBeCalledTimes(1) }) 你也可以像这样做一个更简单的测试,不要忘记jest.useFakeTimers() 的参数,它是可选的,但可以改变一切。
【解决方案3】:

如果您在代码中这样做:

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

【讨论】:

  • mocking lodash debounce 似乎是移动
  • 对我有用的是:jest.mock('lodash/debounce', () =&gt; jest.fn(fn =&gt; fn));
【解决方案4】:

我喜欢这个更容易失败的类似版本:

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 v27 并遇到无限递归错误,请参阅:stackoverflow.com/a/64336022/4844024
  • “更容易失败”是什么意思?你能详细说明一下吗?
  • 我的意思是更容易测试返回错误结果的场景。在这种情况下,如果我们将 jest.advanceTimersByTime() 设置为 600,单元测试将失败,这让我们感到舒适,因为 debounce 函数会做正确的事情,因为它会被调用两次。
【解决方案5】:

另一种方法是刷新 debounce 函数以使其立即执行:

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

【讨论】:

    【解决方案6】:

    使用现代假计时器(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提供源代码

    【讨论】:

      【解决方案7】:

      花了很多时间弄清楚……终于成功了……

      jest.mock('lodash', () => {
          const module = jest.requireActual('lodash');
          module.debounce = jest.fn(fn => fn);
          return module;
      });
      

      【讨论】:

        猜你喜欢
        • 2019-06-16
        • 1970-01-01
        • 1970-01-01
        • 2020-01-07
        • 2021-10-13
        • 2015-03-03
        • 2015-07-25
        • 1970-01-01
        • 2021-06-30
        相关资源
        最近更新 更多