Async & Promises
Patterns for mocking async APIs: resolving with data, rejecting, delaying, hanging, and asserting against the captured Promise.
Resolve / reject
mock.setup.fetch.toResolveWith({ data: 42 })
mock.setup.fetch.toResolve() // resolves with undefined
mock.setup.fetch.toRejectWith(new Error('network'))Sequential results
Multiple calls, each with a different resolved value. Last-sticky by default:
mock.setup.fetch.toResolveInOrder(
{ page: 1, rows: ['a'] },
{ page: 2, rows: ['b'] },
{ page: 3, rows: ['c'] },
)
await mock.fetch() // { page: 1, rows: ['a'] }
await mock.fetch() // { page: 2, rows: ['b'] }
await mock.fetch() // { page: 3, rows: ['c'] }
await mock.fetch() // sticky-last: { page: 3, rows: ['c'] }Fall back to a different value:
mock.setup.fetch.toResolveInOrder([{ a: 1 }, { b: 2 }], { then: null })Or cycle:
mock.setup.fetch.toResolveInOrder([{ a: 1 }, { b: 2 }], { cycle: true })Mix resolve and reject
Use the sequence-aware setups — no need to manually stage once() chains:
mock.setup.fetch.toRejectInOrder(new Error('first'), new Error('second'))Or interleave with when:
mock.setup.fetch.when('/healthy').toResolveWith({ ok: true })
mock.setup.fetch.when('/broken').toRejectWith(new Error('500'))Timeouts — toResolveAfter / toRejectAfter
Delay the resolution. Pairs with fake timers (vitest, jest, or deride/clock):
mock.setup.fetch.toResolveAfter(100, { data: 42 })
const p = mock.fetch('/x') // pending
// ... advance time by 100ms ...
await p // { data: 42 }Vitest fake timers
import { vi } from 'vitest'
vi.useFakeTimers()
mock.setup.fetch.toResolveAfter(1000, 'ok')
const p = mock.fetch()
await vi.advanceTimersByTimeAsync(1000)
expect(await p).toBe('ok')
vi.useRealTimers()deride/clock
import { useFakeTimers } from 'deride/clock'
const clock = useFakeTimers()
try {
mock.setup.fetch.toResolveAfter(1000, 'ok')
const p = mock.fetch()
clock.tick(1000)
clock.flushMicrotasks()
expect(await p).toBe('ok')
} finally {
clock.restore()
}toHang() — never-settling Promise
For testing timeout/cancellation code paths:
mock.setup.fetch.toHang()
const result = await Promise.race([
mock.fetch('/slow'),
new Promise<string>(resolve => setTimeout(() => resolve('timeout'), 500)),
])
expect(result).toBe('timeout')Under fake timers, runAll() does not force toHang() Promises to resolve — they genuinely don't settle.
Awaiting the captured Promise
expect.called.withReturn(x) asserts the return exists but can't hand it back. For follow-up operations, read from the spy:
mock.setup.fetch.toResolveWith({ id: 1 })
mock.fetch('/x')
const result = await mock.spy.fetch.lastCall!.returned
// result === { id: 1 }Useful when:
- The test needs to assert against the resolved value in multiple ways
- A helper needs to forward the captured value to another setup
- Debugging:
await mock.spy.x.lastCall?.returnedoften reveals what the code actually received
Asserting on thrown / rejected Promises
Synchronous throws are captured in CallRecord.threw:
mock.setup.save.toThrow('bang')
try { mock.save() } catch {}
mock.expect.save.called.threw()
mock.expect.save.called.threw('bang')
mock.expect.save.called.threw(Error)Promise rejections are NOT captured in threw — they're captured as the resolved value (the rejected Promise). If you want to assert on the rejection:
mock.setup.fetch.toRejectWith(new Error('oops'))
mock.fetch() // returns a rejected Promise, but no sync throw
// The Promise IS recorded in CallRecord.returned
const promise = mock.spy.fetch.lastCall!.returned as Promise<unknown>
await expect(promise).rejects.toThrow('oops')Async iterators
For code that iterates with for await:
mock.setup.stream.toAsyncYield(1, 2, 3)
for await (const v of mock.stream()) {
console.log(v) // 1, 2, 3
}Throw partway through:
mock.setup.stream.toAsyncYieldThrow(new Error('drained'), 1, 2)
const collected: number[] = []
try {
for await (const v of mock.stream()) collected.push(v)
} catch (err) {
expect((err as Error).message).toBe('drained')
}
expect(collected).toEqual([1, 2])Node-style callbacks
For APIs that take a callback as the last argument:
mock.setup.load.toCallbackWith(null, 'data')
mock.load('file.txt', (err, data) => {
// err === null, data === 'data'
})toCallbackWith finds the last function argument and invokes it with the provided args.
Time-warp legacy callbacks
For callback APIs with long built-in timeouts that you want to accelerate without fake timers:
mock.setup.pollWithRetry.toTimeWarp(0)
mock.pollWithRetry(10_000, (result) => {
// callback fires immediately rather than after 10s
})See also
- Configuring behaviour — full
setup.*reference deride/clock— fake timers sub-path- Time & timers recipes