Testing & Code Quality

Testing Async Code

Async tests must wait for the work they assert on. Promise returns, async functions, fake timers, microtasks, cancellation, and unhandled rejections all affect test reliability.
  • Return or await the async work
  • Rejection paths need explicit assertions
  • Fake timers control scheduled work
  • Microtasks and timers are different queues
  • Unhandled rejections should fail tests
Async success and failure
it("loads user", async () => {
  await expect(loadUser("u1")).resolves.toMatchObject({ id: "u1" })
})

it("rejects invalid id", async () => {
  await expect(loadUser("")).rejects.toThrow("Invalid")
})
The test awaits both success and failure assertions.
Fake timers for debounce
import { afterEach, expect, it, vi } from "vitest"

afterEach(() => vi.useRealTimers())

it("debounces search", () => {
  vi.useFakeTimers()
  const fn = vi.fn()
  const search = debounce(fn, 300)

  search("a")
  search("ab")
  vi.advanceTimersByTime(299)
  expect(fn).not.toHaveBeenCalled()

  vi.advanceTimersByTime(1)
  expect(fn).toHaveBeenCalledWith("ab")
})
The test controls time instead of waiting 300 real milliseconds.
Sources
  • Vitest DocumentationTesting async code
  • Node.js Documentation and Learn Node.jsTesting and timers
  • MDN JavaScript Guide and ReferencePromises and async functions