用玩笑测试自定义 UseInterval 挂钩

Testing a custom UseInterval hook with jest

我的钩子是;

function useInterval() {
  const ref: MutableRefObject<NodeJS.Timer | null > = useRef(null);
  function set(callback: () => void, delay: number) {
    ref.current = setInterval(callback, delay)
  }
  function clear() {
    if (ref.current) {
      clearInterval(ref.current)
      ref.current = null
    }
  }
  return { set, clear }
}

我的测试是;

it("set: This should be called 10 times", () => {
    var callback = jest.fn();
    jest.useFakeTimers()
    const { result } = renderHook(() => hooks.useInterval())
    act(() => {
        result.current.set(() => { callback }, 100)    
        jest.advanceTimersByTime(1000);
    })
    expect(callback).toHaveBeenCalledTimes(10);
    jest.useRealTimers()
})

renderHook()act() 来自 "@testing-library/react-hooks": "^7.0.2"

我不断得到的结果是 0 来自我的 expect() 调用。我似乎无法弄清楚为什么。

如果我只是使用 setInterval() expect() 得到正确的值

it("setInterval", () => {
    var callback = jest.fn();
    jest.useFakeTimers()
    setInterval(callback, 100)
    jest.advanceTimersByTime(1000);
    expect(callback).toHaveBeenCalledTimes(10);
    jest.useRealTimers()
})

我已经尝试以我能想到的所有可能的逻辑方式重新排列这些行。

我注意到无论是否使用 act(),我都会得到相同的结果,很奇怪。

timers: "fake" 或其任何变体 (modern/legacy) 添加到 jest.config.ts 似乎没有任何效果。

显然,testing-library/react-hooks 以某种方式从 jest.useFakeTimers() 中屏蔽了 setInterval(),但我不明白如何实现,因此无法实现我正在寻找的结果。

我的一部分认为我的钩子没有被 jest.useFakeTimers() 击中,因为假计时器没有被全局替换,但我不知道该怎么做。

此外,我正在使用 Typescript。并不是说我认为这会有所作为。

您将匿名函数传递给 set 方法而不是模拟 callback 方法。因此 setInterval 排队的宏任务将调用匿名函数。这就是断言失败的原因。没有什么可以开玩笑的 Config,TypeScript。

例如

useInterval.ts:

import { MutableRefObject, useRef } from 'react';

export function useInterval() {
  const ref: MutableRefObject<ReturnType<typeof setInterval> | null> = useRef(null);
  function set(callback: () => void, delay: number) {
    ref.current = setInterval(callback, delay);
  }
  function clear() {
    if (ref.current) {
      clearInterval(ref.current);
      ref.current = null;
    }
  }
  return { set, clear };
}

useInterval.test.ts:

import { renderHook } from '@testing-library/react-hooks';
import { useInterval } from './useInterval';

describe('70276930', () => {
  beforeAll(() => {
    jest.useFakeTimers();
  });
  afterAll(() => {
    jest.useRealTimers();
  });
  test('should call callback interval', () => {
    const callback = jest.fn();
    const { result } = renderHook(useInterval);
    result.current.set(callback, 100);
    jest.advanceTimersByTime(1000);
    expect(callback).toBeCalledTimes(10);
  });

  test('should clear interval', () => {
    const callback = jest.fn();
    const { result } = renderHook(useInterval);
    result.current.set(callback, 100);
    jest.advanceTimersByTime(100);
    expect(callback).toBeCalledTimes(1);
    result.current.clear();
    jest.advanceTimersByTime(100);
    expect(callback).toBeCalledTimes(1);
  });
});

测试结果:

 PASS  examples/70276930/useInterval.test.ts (7.541 s)
  70276930
    ✓ should call callback interval (16 ms)
    ✓ should clear interval (1 ms)

----------------|---------|----------|---------|---------|-------------------
File            | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
----------------|---------|----------|---------|---------|-------------------
All files       |     100 |       50 |     100 |     100 |                   
 useInterval.ts |     100 |       50 |     100 |     100 | 9                 
----------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        8.294 s, estimated 9 s

包版本:

"react": "^16.14.0",
"@testing-library/react": "^11.2.2",
"jest": "^26.6.3",