@testing-library/react-hooks 调用 setTimeout 两次

@testing-library/react-hooks calls setTimeout two times

这是我自定义的 React-hook。

import { useEffect, useRef } from 'react'

function useInterval({ callback, interval, delay }) {
  const savedTimerId = useRef<NodeJS.Timeout>()

  useEffect(() => {
    const loop = () => {
      const res = callback()
      const nextIteration = () => {
        savedTimerId.current = setTimeout(loop, interval)
      }
      if (res instanceof Promise) {
        res.then(nextIteration)
      } else {
        nextIteration()
      }
    }
    let delayedTimerId: NodeJS.Timeout
    if (!delay) {
      loop()
    } else {
      delayedTimerId = setTimeout(loop, delay)
    }
    return () => {
      // @ts-ignore
      clearTimeout(savedTimerId.current)
      if (delayedTimerId) {
        clearTimeout(delayedTimerId)
      }
    }
  }, [callback, interval, delay])
}

export { useInterval }

这是单元测试

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

describe("Test scenarios for 'useInterval' hook", () => {
  jest.useFakeTimers()

  it("Should call 'callback' once", () => {
    const callback = jest.fn()
    const interval = 10000
    const params = { callback, interval }
    renderHook(() => useInterval(params))
    expect(setTimeout).toHaveBeenCalledTimes(1)
    expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), interval)
  })
})

但这就是输出

Error: expect(jest.fn()).toHaveBeenCalledTimes(expected)

Expected number of calls: 1
Received number of calls: 2

我调试了这段代码。我发现在 useInterval 调用之前已经触发了一些东西 setTimeout

似乎在内部调用了 setTimeout。做错了什么?有什么想法吗?

您完全正确,@testing-library/react-hooks 正在调用 setTimeout,您可以通过以下方式确认:

jest.useFakeTimers()
renderHook(() => {})
expect(setTimeout).toHaveBeenCalledTimes(1)

您最好关注 callback 被调用的次数,而不是 setTimeout:

afterEach(() => {
  jest.clearAllMocks();
  jest.useRealTimers();
});

describe("Test scenarios for 'useInterval' hook", () => {
  it("Should call 'callback' immediately", () => {
    jest.useFakeTimers()

    const callback = jest.fn()
    const interval = 10000
    const params = { callback, interval }
    renderHook(() => useInterval(params))

    expect(callback).toHaveBeenCalledTimes(1)
  })

  it("Should call 'callback' repeatedly", () => {
    jest.useFakeTimers()

    const callback = jest.fn()
    const interval = 10000
    const params = { callback, interval }
    renderHook(() => useInterval(params))

    jest.advanceTimersByTime(interval * 2)

    // Expect 3 calls: 1 immediate call and 2 interval calls:
    expect(callback).toHaveBeenCalledTimes(3)
  })
})