Control helpers
Each spied method gets helpers chosen by its return type. calledWith / mustBeCalledWith dispatch by argument, and the type-specific helpers configure the result.
Key order does not matter
Arguments are matched by a serialized key, and object keys are sorted before it is built — so calledWith({ id: 1, name: 'a' }) matches a call made with { name: 'a', id: 1 }. The two are the same argument, and the order a literal happened to be written in is not something a test should depend on.
Synchronous methods
// standard vi.fn() API works as-is
myService.getName.mockReturnValue('Fake Name');
// return a value only for specific arguments
myService.getName.calledWith(1).mockReturnValue('Fake Name');
expect(myService.getName(1)).toBe('Fake Name');
expect(myService.getName(2)).toBeUndefined();
// throw if called with the "wrong" arguments
myService.getName.mustBeCalledWith(1).mockReturnValue('Fake Name');
expect(() => myService.getName(2)).toThrow();Asymmetric matchers in calledWith
calledWith / mustBeCalledWith accept the same asymmetric matchers as expect (expect.any, expect.objectContaining, expect.stringMatching, …). A config that contains at least one matcher is stored as a predicate and matched against the actual arguments at call time, instead of by exact serialization.
myService.getName.calledWith(expect.any(Number)).mockReturnValue('Fake Name');
expect(myService.getName(1)).toBe('Fake Name');
expect(myService.getName(2)).toBe('Fake Name');
myService.save.calledWith(expect.objectContaining({ id: 1 })).mockReturnValue(true);
expect(myService.save({ id: 1, name: 'x' })).toBe(true);Promise-returning methods — resolveWith
myService.getProducts.resolveWith([{ name: 'Product 1' }]);
await expect(myService.getProducts()).resolves.toEqual([{ name: 'Product 1' }]);
myService.getProducts.rejectWith('FAKE ERROR');
// per-call values, and conditional-by-args
myService.getProducts.resolveWithPerCall([{ value: ['a'] }, { value: ['b'] }]);
myService.getProducts.calledWith(1).resolveWith(['one']);Inspecting promise outcomes — mock.settledResults
Every spied method exposes mock.settledResults: one index-aligned entry per call, recording how that call's returned promise eventually settled. Vitest tracks this natively; on Bun (bun:test) and node:test it is provided by a built-in polyfill, so the surface is identical across all three runtimes.
myService.getProducts.resolveWith([{ name: 'Product 1' }]);
await myService.getProducts();
expect(myService.getProducts.mock.settledResults).toEqual([{ type: 'fulfilled', value: [{ name: 'Product 1' }] }]);
myService.getProducts.rejectWith('FAKE ERROR');
await myService.getProducts().catch(() => undefined);
expect(myService.getProducts.mock.settledResults).toContainEqual({ type: 'rejected', value: 'FAKE ERROR' });Each entry is { type: 'fulfilled' | 'incomplete' | 'rejected', value }. A call whose promise is still pending is recorded as incomplete until it settles.
Resetting spies — clearAutoSpy / resetAutoSpy
Reset every spy inside an assembled spy with one call, instead of reaching for mockClear / mockReset on each method by hand. Both work on createSpyFromClass spies and createAutoMock proxies, and cover method spies and accessor spies.
import { clearAutoSpy, resetAutoSpy } from 'vitest-auto-spy';
// clears recorded calls only — configured returns are kept
clearAutoSpy(myService);
// clears calls AND reverts configuration to pristine
resetAutoSpy(myService);resetAutoSpy reverts both the library config (calledWith / resolveWith / nextWith / …) and a bare return value set directly on a spy (myService.getName.mockReturnValue('x')) — after a reset the method returns undefined again until reconfigured.
Observable methods & properties — nextWith
Enabled by importing the rxjs layer once (import 'vitest-auto-spy/rxjs';). See Runtimes → RxJS.
myService.getProducts$.nextWith([{ name: 'Product 1' }]); // emit, stream stays open
myService.getProducts$.nextOneTimeWith([{ name: 'X' }]); // emit once, then complete
myService.getProducts$.throwWith('FAKE ERROR'); // error the stream
myService.getProducts$.complete(); // complete the streamA precise sequence — nextWithValues
nextWithValues(configs) emits the entries in order, and stops at the first { complete: true }. Anything pushed onto the backing subject afterwards is merged in until that completion arrives.
myService.getProducts$.nextWithValues([
{ value: [{ name: 'Product 1' }] },
{ value: [{ name: 'Product 2' }], delay: 100 },
{ complete: true },
]);ValueConfig
| Shape | Effect |
|---|---|
{ value, delay? } | emit value (after delay ms, if given) |
{ errorValue, delay? } | error the stream with errorValue (after delay ms) |
{ complete?, delay? } | complete the stream — complete: false emits nothing |
delay is milliseconds and is applied with RxJS's own delay() / timer(), so under fake timers you have to advance the clock: advanceTimers(ms) does that and drains the microtasks the emission queues.
A fresh stream per call — nextWithPerCall
nextWithPerCall(configs) hands the n-th call the n-th entry, and returns one ReplaySubject per entry so a test can push more values into a specific call later.
const [first$, second$] = myService.watch$.nextWithPerCall([
{ value: 'a' },
{ value: 'b', doNotComplete: true },
]);
expect(await firstValueFrom(myService.watch$())).toBe('a');
// the second call's stream stays open, so it can be driven further
second$.next('b2');Each per-call stream completes after its first value unless the entry sets doNotComplete: true. ValueConfigPerCall is { value, delay?, doNotComplete? }.
Manual control — returnSubject
returnSubject() hands back the ReplaySubject behind the spy, for the cases the helpers do not cover:
const subject = myService.getProducts$.returnSubject();
subject.next([{ name: 'Product 1' }]);
subject.error(new Error('boom'));It is a ReplaySubject, so a subscriber that arrives late still sees the values already pushed.
Full reference, plus the standalone createObservableWithValues builder: Runtimes → RxJS.