vitest-auto-spy
Auto-generate fully-typed test spies from a class — across any Vitest-compatible runtime and framework.
The only auto-spy library that reads a class and gives a fully-typed spy of every method
with return-type-aware helpers — resolveWith / rejectWith for Promises, nextWith /
throwWith for RxJS Observables, and calledWith / mustBeCalledWith for argument matching. Or
skip the class entirely and mock straight from a type or interface with createAutoMock<T>() and
recursive mockDeep<T>(). Runs on Vitest, Bun (bun:test) and node:test behind one
identical API, with RxJS spies and Angular / NestJS / React / Vue·Pinia / Svelte recipes
(availability). A drop-in replacement for
jest-auto-spies — same API.
- Reads a class and generates a typed spy for every method — no hand-written
vi.fn()lists - Or mock from a type/interface alone —
createAutoMock<T>(), no class required - One
MockAdaptercore — Vitest, Bun andnode:test, identical API on each - Framework recipes: Angular, NestJS, React, Vue/Pinia and Svelte
- Return-type-aware helpers — sync,
Promise, andObservableall get the right API -
calledWith/mustBeCalledWithargument dispatch - First-class RxJS
Observablespying (nextWith,nextWithValues,throwWith, …) - Getter / setter spies via
accessorSpies - DI & mocking utilities —
provideAutoSpy/injectSpy(Angular, NestJS, Vue),createFunctionSpy,mockReadonlyPropfor signals - Console spies —
import { consoleInfoSpy } from 'vitest-auto-spy/console'silencesconsoleand asserts its calls - 100% test coverage, zero runtime dependencies (in-tree arg serializer, no
javascript-stringify)
Table of contents
- Install
- Availability
- Quick start
- Why
- How it works (and what it won't spy)
- Comparison
- Migrating from jest-auto-spies
- Configuration
- Auto-mock by type (no class needed)
- Synchronous methods
- Promise-returning methods
- Observable methods & properties
- Getters & setters
- Framework adapters
- Utilities
- API reference
- FAQ & troubleshooting
- Versioning
- Contributing
- Acknowledgements
- License
Install
npm i -D vitest-auto-spy
Requirements
| Tool | Minimum |
|---|---|
| Node.js | ≥ 18 |
| Vitest | ≥ 1.0 (required peer) |
| TypeScript | ≥ 4.7 for the typed helpers (plain JS works too, just untyped) |
Ships dual ESM + CommonJS with bundled .d.ts types, so it drops into both import- and
require-style test setups.
Peer dependencies
All peers are provided by your project; rxjs and @angular/core are optional — install
them only for the matching entry point. The package itself has zero runtime dependencies.
| Peer | Needed for | Optional? |
|---|---|---|
vitest |
the default runner | no |
rxjs |
vitest-auto-spy/rxjs observable spies (and Spy<T> type-checking) — >=7, no upper bound (the rxjs 8 line included) |
yes |
@angular/core |
vitest-auto-spy/angular helpers |
yes |
Availability
All entry points are published. The Vitest / Bun /
node:testruntimes, the RxJS layer, and the Angular / NestJS / React / Vue·Pinia / Svelte recipes all ship as importable entry points — one identical API across every runner and framework.
| Entry point | Status |
|---|---|
vitest-auto-spy · vitest-auto-spy/rxjs · vitest-auto-spy/angular |
Published |
vitest-auto-spy/bun · vitest-auto-spy/node |
Published |
vitest-auto-spy/nestjs · /react · /vue · /svelte · /console |
Published |
Quick start
Pass a class — every method becomes a typed spy, and the constructor is never called (no side effects). The helper you get on each method matches its return type:
import { beforeEach, expect, it } from 'vitest';
import { createSpyFromClass, type Spy } from 'vitest-auto-spy';
class UserService {
getName(id: number): string {
return 'real name';
}
async getUser(id: number): Promise<{ id: number; name: string }> {
return fetchUser(id);
}
}
let userService: Spy<UserService>;
beforeEach(() => {
userService = createSpyFromClass(UserService); // every method is now a spy
});
it('stubs each method with the right helper for its return type', async () => {
userService.getName.mockReturnValue('Ada'); // sync
userService.getUser.resolveWith({ id: 1, name: 'Ada' }); // Promise helper
expect(userService.getName(1)).toBe('Ada');
await expect(userService.getUser(1)).resolves.toEqual({ id: 1, name: 'Ada' });
expect(userService.getName).toHaveBeenCalledWith(1);
});
No class, only a TypeScript type? Reach for
createAutoMock<T>().
Why
Manually mocking a service is tedious and brittle:
// 😫 the old way
const userService = {
getUser: vi.fn(),
getUserList: vi.fn(),
// ...one line per method, kept in sync by hand
};
createSpyFromClass reads the class and generates a typed spy for every method:
// 😎 the auto-spy way
let userService: Spy<UserService>;
beforeEach(() => {
userService = createSpyFromClass(UserService);
});
Spy<UserService> exposes each method as a vi.fn() plus the right helpers based on
the method's return type (sync / Promise / Observable).
How it works (and what it won't spy)
createSpyFromClass(MyService) reads MyService.prototype and walks the prototype chain — it
never news the class. Concretely:
- The class is never instantiated. The constructor and its side effects (HTTP clients, DB
connections,
inject()calls) never run — you pass the class itself, not an instance. - Inherited methods are spied too, all the way up the prototype chain.
- Each method is replaced by a fresh spy carrying the helpers that match its return type:
sync →
mockReturnValue/calledWith;Promise→resolveWith/rejectWith;Observable→nextWith/throwWith/ … .
What it won't auto-discover — by design, because these aren't prototype methods:
- Arrow-function class fields (
doThing = () => {}) are instance properties set in the constructor, so prototype scanning can't see them. Use regular methods, list them explicitly, or mock them by hand. (Same constraint asjest-auto-spies.) - Getters / setters are skipped unless named in
gettersToSpyOn/settersToSpyOn— see Getters & setters. - Plain data properties carry no value until you set one; auto-spy mocks behaviour
(methods), not state. To mock by type including properties, use
createAutoMock.
Entry points & runtimes
The library ships a framework-agnostic core plus runtime and framework layers, so a plain Node / Bun / React / Vue project pulls neither rxjs nor Angular into its runtime bundle:
| Import | Provides | Pulls in | Status |
|---|---|---|---|
vitest-auto-spy |
createSpyFromClass, createAutoMock, createFunctionSpy, sync + promise + accessor spies, errorHandler, types |
vitest |
|
vitest-auto-spy/rxjs |
observable spies (nextWith, nextWithValues, observablePropsToSpyOn, …) + createObservableWithValues |
rxjs |
|
vitest-auto-spy/angular |
provideAutoSpy, injectSpy, mockReadonlyProp*, mockAccessorsProp |
@angular/core |
|
vitest-auto-spy/bun |
the same core, driven by Bun's bun:test mocks |
bun:test |
|
vitest-auto-spy/node |
the same core, driven by node:test's mock.fn() |
node:test |
|
vitest-auto-spy/nestjs |
provideAutoSpy, injectSpy for Test.createTestingModule |
— (your @nestjs/*) |
|
vitest-auto-spy/react |
the core, with a natural import for React Testing Library suites | — (your react) |
|
vitest-auto-spy/vue |
provideAutoSpy for global.provide + Pinia store spying |
— (your vue/pinia) |
|
vitest-auto-spy/svelte |
the core, with a natural import for Svelte suites | — (your svelte) |
|
vitest-auto-spy/console |
consoleInfoSpy & friends — silent typed spies over the global console, installed on import |
vitest |
all entry points published (see Availability).
The framework subpaths import nothing from their framework — the helpers are structural, so
@nestjs/*,react,vue/piniaandsveltestay your own (already-present) dev dependencies and never reach this package's runtime bundle.
import { createSpyFromClass } from 'vitest-auto-spy';
import 'vitest-auto-spy/rxjs'; // once (e.g. in your test setup) — enables observable spies
import { provideAutoSpy, injectSpy } from 'vitest-auto-spy/angular';
Runtimes
The core is runner-agnostic behind a MockAdapter: pick the entry that matches your test
runner — the public API (createSpyFromClass, calledWith, resolveWith, nextWith, …) is
identical across all three.
import { createSpyFromClass } from 'vitest-auto-spy'; // Vitest (default, zero-config)
import { createSpyFromClass } from 'vitest-auto-spy/bun'; // Bun — bun:test
import { createSpyFromClass } from 'vitest-auto-spy/node'; // node:test
Only the auto-spy helpers are normalised across runtimes; native mock methods stay the runner's own —
mockReturnValueon Vitest/Bun,spy.method.mock.mockImplementationonnode:test. Each entry registers its adapter on import, so import the one matching your runner.
Using an observable spy (
observablePropsToSpyOn,nextWith, …) without importingvitest-auto-spy/rxjsthrows a clear hint telling you to add that import.The decoupling is at the runtime level. The core's type surface (
Spy<T>) still references rxjs types, so keeprxjsavailable for type-checking (it's normally already a devDependency); none of it reaches your runtime bundle.The same inversion-of-control applies to the test runner: the core no longer imports
vitestdirectly —vi.fn()/vi.spyOn()sit behind aMockAdapterthat thevitest-auto-spyentry registers by default, so it stays zero-config. This is the groundwork for running the exact same core on other Vitest-compatible runners.
Comparison
| Library | Reads a class? | Return-type-aware helpers? | Runtimes | We win on |
|---|---|---|---|---|
| vitest-auto-spy | Vitest · Bun · node:test | — | ||
| jest-auto-spies | Jest only | Vitest/Bun/Node successor, same API — direct migration path | ||
| @bugsplat/vitest-auto-spies | Vitest only | Same class-based API plus Bun & node:test, type-only createAutoMock, framework recipes (Angular/NestJS/React/Vue/Svelte), console spies, and zero runtime deps (it depends on @hirez_io/auto-spies-core) |
||
| vitest-mock-extended | (Proxy) | Vitest | Return-type ergonomics and reading a real class (we also ship a Proxy mode: createAutoMock) |
|
| @golevelup/ts-vitest | partial | Vitest | Typed Promise/Observable helpers + explicit class→spy + mustBeCalledWith |
|
| sinon | (manual) | Any | Auto-generated + fully typed vs. manual + loosely typed |
The pitch: the only auto-spy library that reads a class and gives a fully-typed spy of
every method with return-type-aware control helpers (resolveWith / nextWith / calledWith) —
across any Vitest-compatible runtime and framework.
Migrating from jest-auto-spies
The public API is intentionally identical. In most projects the migration is a find-and-replace of the import:
- import { createSpyFromClass, provideAutoSpy } from 'jest-auto-spies';
+ import { createSpyFromClass } from 'vitest-auto-spy';
+ import { provideAutoSpy } from 'vitest-auto-spy/angular';
+ import 'vitest-auto-spy/rxjs'; // once, if you use observable spies
The only API-shape change from jest-auto-spies is that the Angular helpers and the
observable layer live behind the /angular and /rxjs subpaths (see Entry points & runtimes).
This also covers migrating from @bugsplat/vitest-auto-spies,
which re-exports the same jest-auto-spies API — the swap is identical, and you gain Bun /
node:test, createAutoMock, framework recipes and console spies on top.
| jest-auto-spies | vitest-auto-spy | Status |
|---|---|---|
createSpyFromClass |
createSpyFromClass |
identical |
provideAutoSpy |
provideAutoSpy |
identical |
calledWith / mustBeCalledWith |
same | identical |
calledWith(...).returnValue(v) |
same — .returnValue and .mockReturnValue both work |
identical |
resolveWith / rejectWith / resolveWithPerCall |
same | identical |
nextWith / nextOneTimeWith / nextWithValues / nextWithPerCall |
same | identical |
throwWith / complete / returnSubject |
same | identical |
accessorSpies.getters/setters |
same | identical |
createObservableWithValues |
same | identical |
| underlying mock | jest.fn() → vi.fn() |
swapped |
Just make sure your tests run under Vitest, and (for Angular) that TestBed is set up.
Configuration
// 1. all methods (default)
createSpyFromClass(MyService);
// 2. only these methods
createSpyFromClass(MyService, ['getName', 'getAge']);
// 3. full config object
createSpyFromClass(MyService, {
methodsToSpyOn: ['getName'],
observablePropsToSpyOn: ['products
Auto-mock by type (no class needed)
createSpyFromClass reads a real class's prototype. When you only have a TypeScript interface or
type (no runtime class), use createAutoMock<T>() — it builds the spy lazily from the type alone,
via a Proxy:
import { createAutoMock } from 'vitest-auto-spy';
interface UserService {
getName(id: number): string;
getUser(id: number): Promise<User>;
apiUrl: string;
}
// Before — needs a concrete class:
// const svc = createSpyFromClass(UserServiceClass);
// After — type only, no class:
const svc = createAutoMock<UserService>();
Every accessed method becomes a decorated spy with the same typed control helpers as
createSpyFromClass, materialized lazily and cached (same reference on re-access):
svc.getName.calledWith(1).mockReturnValue('Ada'); // sync, arg-matched
svc.getUser.resolveWith({ id: 1, name: 'Ada' }); // promise helper
expect(svc.getName(1)).toBe('Ada');
await expect(svc.getUser(1)).resolves.toEqual({ id: 1, name: 'Ada' });
Seed concrete values or implementations with the optional overrides argument (seeded keys are
returned as-is, never turned into spies):
const svc = createAutoMock<UserService>({ apiUrl: 'https://api.test' });
expect(svc.apiUrl).toBe('https://api.test'); // or assign: svc.apiUrl = '...'
Caveat: with only a type at runtime, methods and plain properties are indistinguishable on
access — an un-seeded property read returns a spy. Seed real property values via overrides
(or assignment) to get them back verbatim.
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();
Promise-returning methods
myService.getProducts.resolveWith([{ name: 'Product 1' }]);
await expect(myService.getProducts()).resolves.toEqual([{ name: 'Product 1' }]);
myService.getProducts.rejectWith('FAKE ERROR');
await expect(myService.getProducts()).rejects.toBe('FAKE ERROR');
// per-call values, and conditional-by-args
myService.getProducts.resolveWithPerCall([{ value: ['a'] }, { value: ['b'] }]);
myService.getProducts.calledWith(1).resolveWith(['one']);
Observable-returning methods & Observable properties
Both spied methods that return an Observable and spied properties of type
Observable get the same control surface. Enable them by importing the rxjs layer once:
import 'vitest-auto-spy/rxjs';
myService.getProducts$.nextWith([{ name: 'Product 1' }]); // emit, stream stays open
myService.getProducts$.nextOneTimeWith([{ name: 'X' }]); // emit one value, then complete
myService.getProducts$.throwWith('FAKE ERROR'); // error the stream
myService.getProducts$.complete(); // complete the stream
// emit a precise sequence — values, errors, completion, optional delays
myService.getProducts$.nextWithValues([
{ value: [{ name: 'Product 1' }] },
{ errorValue: 'FAKE ERROR' },
{ complete: true },
]);
// a fresh stream per call
myService.getProducts$.nextWithPerCall([{ value: ['a'] }, { value: ['b'] }]);
// grab the underlying Subject for full manual control
const subject = myService.getProducts$.returnSubject();
subject.next([{ name: 'manual' }]);
calledWith(...) / mustBeCalledWith(...) also chain into the observable helpers:
myService.getProducts$.calledWith(1).nextWith([{ name: 'Product 1' }]);
Standalone observable builder
import { createObservableWithValues } from 'vitest-auto-spy/rxjs';
const fake$ = createObservableWithValues([{ value: 1 }, { value: 2 }, { complete: true }]);
// or get the subject too
const { values$, subject } = createObservableWithValues([{ value: 1 }], { returnSubject: true });
Getters & setters
const spy = createSpyFromClass(MyService, {
gettersToSpyOn: ['userName'],
settersToSpyOn: ['userName'],
});
// configure / assert the getter
spy.accessorSpies.getters.userName.mockReturnValue('Fake Name');
expect(spy.userName).toBe('Fake Name');
// assert the setter was called
spy.userName = 'New Name';
expect(spy.accessorSpies.setters.userName).toHaveBeenCalledWith('New Name');
Framework adapters
The core is framework-agnostic — createSpyFromClass / createAutoMock work in any test. The
subpaths below add a natural import and, where the framework has class DI, a tiny provide* helper.
None of them pull the framework into this package; they're recipes over the same core.
The Angular, NestJS, React, Vue/Pinia and Svelte entry points are all published
(Availability). Each is a thin recipe over the same core, so you can equally copy it
using the core vitest-auto-spy import directly.
NestJS
Use provideAutoSpy to register a fully-mocked service in a TestingModule, then injectSpy to
pull it back out already typed as Spy<T>. @nestjs/common / @nestjs/testing are your own
(optional) peers — the helper imports neither:
import { Test, type TestingModule } from '@nestjs/testing';
import { provideAutoSpy, injectSpy } from 'vitest-auto-spy/nestjs';
import { beforeEach, expect, it } from 'vitest';
import { AuthService } from './auth.service';
import { UserService } from './user.service';
let moduleRef: TestingModule;
let userServiceSpy: Spy<UserService>;
beforeEach(async () => {
moduleRef = await Test.createTestingModule({
providers: [AuthService, provideAutoSpy(UserService)],
}).compile();
userServiceSpy = injectSpy(moduleRef, UserService);
});
it('logs in a known user', () => {
userServiceSpy.findByEmail.mockReturnValue({ id: 1, name: 'Ada' });
const auth = moduleRef.get(AuthService);
expect(auth.login('ada@example.com')).toBeTruthy();
expect(userServiceSpy.findByEmail).toHaveBeenCalledWith('ada@example.com');
});
React (Testing Library)
React has no DI container, so there's no provide* helper — the recipe is: spy the classes you
own (services, stores, API clients, hook deps), then pass the spy into a Context provider or hook.
The spy is a plain object of spied functions, so it drops straight into value={...}:
import { render, screen } from '@testing-library/react';
import { createSpyFromClass, type Spy } from 'vitest-auto-spy/react';
import { CartContext, Cart } from './cart';
class CartStore {
getItemCount(): number { return 0; }
checkout(token: string): Promise<{ orderId: string }> { /* ... */ }
}
let cart: Spy<CartStore>;
beforeEach(() => {
cart = createSpyFromClass(CartStore); // every method is now a spy
});
it('shows the item count from the injected store', () => {
cart.getItemCount.mockReturnValue(3);
render(
<CartContext.Provider value={cart}>
<Cart />
</CartContext.Provider>,
);
expect(screen.getByText('3 items')).toBeInTheDocument();
});
it('drives async deps and asserts the component called them', async () => {
cart.checkout.resolveWith({ orderId: 'ord_42' });
// ...trigger checkout in the UI...
expect(cart.checkout).toHaveBeenCalledWith('tok_abc');
});
Vue / Pinia
provideAutoSpy(token, Class) returns a { [token]: Spy<T> } map you can spread into
@vue/test-utils' global.provide; for a class-based Pinia store, spy it directly:
import { mount } from '@vue/test-utils';
import { createSpyFromClass, provideAutoSpy } from 'vitest-auto-spy/vue';
// (a) class-based service injected via provide / global.provide
import { UserServiceKey, UserService } from '@/services/user.service';
const provide = provideAutoSpy(UserServiceKey, UserService); // { [UserServiceKey]: Spy<UserService> }
provide[UserServiceKey].getName.mockReturnValue('Fake Name');
const wrapper = mount(UserBadge, { global: { provide } });
expect(provide[UserServiceKey].getName).toHaveBeenCalled();
// (b) class-based Pinia store — every action becomes a spy
import { CartStore } from '@/stores/cart.store';
const store = createSpyFromClass(CartStore);
store.itemCount.mockReturnValue(3); // sync action/getter
store.checkout.resolveWith({ orderId: 'ord_42' }); // async action (Promise)
await store.checkout('tok_abc');
expect(store.checkout).toHaveBeenCalledWith('tok_abc');
Svelte
Svelte has no class-based DI, so it's a recipe: keep your logic in plain class-based
services/stores, spy the class, and hand the spy to the component the same way it receives the real
one (props, context, or a mocked module):
import { render } from '@testing-library/svelte';
import { createSpyFromClass } from 'vitest-auto-spy/svelte';
import Cart from './Cart.svelte';
import { CartStore } from './cart-store';
it('shows the cart total from the store', () => {
const cartStore = createSpyFromClass(CartStore); // every method is a spy
cartStore.total.mockReturnValue(42);
cartStore.priceOf.calledWith('apple').mockReturnValue(7);
render(Cart, { props: { store: cartStore } });
expect(cartStore.total).toHaveBeenCalled();
});
Angular

provideAutoSpy is the shorthand for providing an auto-spy in a TestBed:
import { provideAutoSpy, injectSpy } from 'vitest-auto-spy/angular';
TestBed.configureTestingModule({
providers: [
provideAutoSpy(MyService),
// accepts the same second argument as createSpyFromClass
provideAutoSpy(ApiService, { methodsToSpyOn: ['get', 'post'] }),
],
});
let myService: Spy<MyService>;
beforeEach(() => {
myService = injectSpy(MyService);
});
The spies are change-detection agnostic, so they work in both zoneless and
zone.js Angular projects — nothing here touches NgZone or change detection.
You only need the usual Vitest + Angular wiring:
@analogjs/vite-plugin-angular
plus a TestBed setup file (e.g. @analogjs/vitest-angular's setupTestBed()).
Lazy by default. provideAutoSpy builds each method spy on first access
(lazySpies: true), since Angular tests typically spy a wide service but call
only a few of its methods — roughly 4× faster spy assembly (≈8× on a
20-method service). Behaviour is unchanged; pass { lazySpies: false } to build
every spy eagerly.
Signal / readonly property mocking (bonus)
import { mockReadonlyProp, mockReadonlyPropGetter, mockAccessorsProp } from 'vitest-auto-spy/angular';
mockReadonlyProp(service, 'isReady', true); // static value (incl. signals)
mockReadonlyPropGetter(service, 'label', () => 'A'); // dynamic getter
mockAccessorsProp(service, 'theme'); // spied get + set
Utilities
Beyond the spy factories, the package ships a set of small standalone helpers. Each one is a
single-purpose utility you can pick up independently — they all ride on the same core:
Utility
Entry point
What it's for
injectSpy(token) / injectSpy(moduleRef, token)
/angular, /nestjs
Pull a provided spy out of the DI container, already typed as Spy<T> — no casting
provideAutoSpy(Class, config?)
/angular, /nestjs, /vue
One-liner { provide, useValue } (or Vue global.provide) that builds the spy for you
createFunctionSpy(name)
core
A single standalone function spy with the full helper set (calledWith, resolveWith, nextWith, …) — no class needed
createAutoMock<T>(overrides?)
core
Proxy-based spy from a type/interface alone (details)
createObservableWithValues(configs, opts?)
/rxjs
Build a fake Observable emitting a precise sequence of values / errors / completion
consoleInfoSpy / consoleWarnSpy / …
/console
Silent typed spies over the global console, installed on import (details)
mockReadonlyProp(obj, prop, value)
/angular
Overwrite a readonly property (incl. Angular signals) with a static value
mockReadonlyPropGetter(obj, prop, getter)
/angular
Same, but backed by a dynamic getter
mockAccessorsProp(obj, prop)
/angular
Redefine a property with spied get + set
errorHandler
core
The mustBeCalledWith argument-mismatch reporter — swap it to customize failure output
A taste of the DI pair — provide the spy, inject it back fully typed:
import { provideAutoSpy, injectSpy } from 'vitest-auto-spy/angular';
TestBed.configureTestingModule({ providers: [provideAutoSpy(UserService)] });
const userService = injectSpy(UserService); // Spy<UserService>, no `as` cast
And a standalone function spy, when there's no class or interface at all:
import { createFunctionSpy } from 'vitest-auto-spy';
const onSave = createFunctionSpy<(id: number) => Promise<void>>('onSave');
onSave.calledWith(1).resolveWith();
Console spies — vitest-auto-spy/console
Importing the entry replaces console.debug / error / info / log / time / timeEnd /
trace / warn with silent, fully-typed spies and exports each one ready to assert — no
vi.spyOn(console, 'info') boilerplate in every suite, no log output polluting the test run:
import { consoleInfoSpy, consoleWarnSpy } from 'vitest-auto-spy/console';
service.doWork();
expect(consoleInfoSpy).toHaveBeenCalledWith('done');
expect(consoleWarnSpy).not.toHaveBeenCalled();
Housekeeping: resetConsoleSpies() clears the recorded calls between tests (Vitest's
clearMocks: true already does that automatically), restoreConsole() puts the original
methods back, and installConsoleSpies() re-installs after a restore.
The spies use the registered MockAdapter — import your runtime entry
(vitest-auto-spy/bun, …/node) before vitest-auto-spy/console and the console spies
are driven by that runner's mocks; with no prior runtime entry the default Vitest adapter is used.
Prefer a fully detached fake instead of touching the real global? createAutoMock<Console>()
gives you a typed, in-memory console to inject into code that takes a logger:
import { createAutoMock } from 'vitest-auto-spy';
const fakeConsole = createAutoMock<Console>();
const service = new ReportService(fakeConsole);
service.doWork();
expect(fakeConsole.info).toHaveBeenCalledWith('done');
API reference
Export
Description
createSpyFromClass(Class, methodsOrConfig?)
Build a fully-typed Spy<T> from a class
createAutoMock<T>(overrides?)
Build a Spy<T> from a type/interface alone (Proxy, no class)
mockDeep<T>(overrides?)
Build a recursive auto-mock — mock.repo.user.find() chains without seeding
resetAutoSpy(spy) / clearAutoSpy(spy)
Reset every spy in an auto-spy at once — reset also reverts return-value config (calledWith and a bare mockReturnValue); clear keeps it
provideAutoSpy(Class, methodsOrConfig?)
Angular / NestJS { provide, useValue } shorthand
provideAutoSpy(token, Class, methodsOrConfig?)
Vue { [token]: Spy<T> } for global.provide
injectSpy(token) (Angular) / injectSpy(moduleRef, token) (NestJS)
Inject typed as Spy<T>
createFunctionSpy(name)
A single standalone function spy with all helpers
createObservableWithValues(configs, opts?)
Build an Observable from value configs
mockReadonlyProp / mockReadonlyPropGetter / mockAccessorsProp
Mock readonly / accessor / signal props
consoleDebugSpy … consoleWarnSpy (/console)
Silent typed spies replacing the global console methods on import
installConsoleSpies() / resetConsoleSpies() / restoreConsole()
Install / clear / undo the console spies
errorHandler
The mustBeCalledWith argument-mismatch error helper
Spied sync method: mockReturnValue, calledWith(...), mustBeCalledWith(...) — calledWith
also matches asymmetric matchers (calledWith(expect.any(Number)), expect.objectContaining({...}))
Spied Promise method: resolveWith, rejectWith, resolveWithPerCall
Spied Observable method / property: nextWith, nextOneTimeWith, nextWithValues,
nextWithPerCall, throwWith, complete, returnSubject
Config (ClassSpyConfiguration): methodsToSpyOn, observablePropsToSpyOn,
gettersToSpyOn, settersToSpyOn, autoSpyAccessors (discover every getter/setter),
lazySpies (materialize method spies on first access — cheaper for wide classes; the provideAutoSpy default on Angular)
ValueConfig (for nextWithValues): { value, delay? } | { errorValue, delay? } | { complete?, delay? }.
FAQ & troubleshooting
"I get X.nextWith is not a function / observable helpers are missing."
Import the rxjs layer once (e.g. in your test setup): import 'vitest-auto-spy/rxjs';. Without it,
requesting an observable spy throws a hint pointing you here.
"My method isn't on the spy."
Auto-discovery only sees prototype methods. Arrow-function class fields (foo = () => {}) and
plain properties aren't included — see How it works. List
getters/setters via gettersToSpyOn / settersToSpyOn.
"Does it construct my class? Will the constructor's side effects run?"
No. createSpyFromClass reads the prototype and never news the class, so constructors (and their
HTTP/DB/inject() side effects) never run.
"I only have an interface/type, not a class."
Use createAutoMock<T>() — it builds the spy lazily from the
type via a Proxy, no runtime class needed.
"Can I use it without TypeScript?"
Yes — the runtime works in plain JS; you just lose the compile-time Spy<T> typing.
"Native mock methods differ between runners."
Only the auto-spy helpers are normalised. Native APIs stay the runner's own (mockReturnValue on
Vitest/Bun, spy.method.mock.mockImplementation on node:test).
Versioning
This package follows Semantic Versioning. Breaking changes to the public API
land only in major releases; see the Changelog for what changed in each version.
Releases are automated from Conventional Commits (see Contributing).
Contributing
Contributions are welcome! Please read CONTRIBUTING.md and the
Code of Conduct. In short:
npm ci
npm test # run the suite once
npm run test:watch # fast local loop — no v8 coverage instrumentation
npm run test:coverage # 100% thresholds enforced (slower; for CI / pre-push)
npm run build
Tip: develop against npm run test:watch — it skips the v8 coverage
instrumentation that test:coverage adds, so the feedback loop is noticeably
faster. Run test:coverage before pushing to confirm the 100% thresholds.
Releases are automated: merging a PR into master bumps the version from the
Conventional Commit types and publishes to npm — see
CONTRIBUTING.md → Releasing.
If this package saved you time, a on GitHub
helps others find it.
Acknowledgements
API and ergonomics are modelled on Shai Reznik's
jest-auto-spies — vitest-auto-spy is its
Vitest-era successor with the same surface, so migrations are (mostly) a find-and-replace. Thanks to
the Vitest, Bun, RxJS and Angular communities whose tooling this builds on.
License
Get in touch: asdalexey.github.io
], // Observable *properties*
gettersToSpyOn: ['userName'],
settersToSpyOn: ['userName'],
});
Auto-mock by type (no class needed)
__INLINE_CODE_186__ reads a real class's prototype. When you only have a TypeScript interface or type (no runtime class), use __INLINE_CODE_187__ — it builds the spy lazily from the type alone, via a __INLINE_CODE_188__:
__CODE_BLOCK_8__Every accessed method becomes a decorated spy with the same typed control helpers as __INLINE_CODE_189__, materialized lazily and cached (same reference on re-access):
__CODE_BLOCK_9__Seed concrete values or implementations with the optional __INLINE_CODE_190__ argument (seeded keys are returned as-is, never turned into spies):
__CODE_BLOCK_10__Caveat: with only a type at runtime, methods and plain properties are indistinguishable on access — an un-seeded property read returns a spy. Seed real property values via __INLINE_CODE_191__ (or assignment) to get them back verbatim.
Synchronous methods
__CODE_BLOCK_11__Promise-returning methods
__CODE_BLOCK_12__Observable-returning methods & Observable properties
Both spied methods that return an __INLINE_CODE_192__ and spied properties of type __INLINE_CODE_193__ get the same control surface. Enable them by importing the rxjs layer once:
__CODE_BLOCK_13__ __CODE_BLOCK_14____INLINE_CODE_194__ / __INLINE_CODE_195__ also chain into the observable helpers:
__CODE_BLOCK_15__Standalone observable builder
__CODE_BLOCK_16__Getters & setters
__CODE_BLOCK_17__Framework adapters
The core is framework-agnostic — __INLINE_CODE_196__ / __INLINE_CODE_197__ work in any test. The subpaths below add a natural import and, where the framework has class DI, a tiny __INLINE_CODE_198__ helper. None of them pull the framework into this package; they're recipes over the same core.
The Angular, NestJS, React, Vue/Pinia and Svelte entry points are all published (Availability). Each is a thin recipe over the same core, so you can equally copy it using the core __INLINE_CODE_199__ import directly.
NestJS
Use __INLINE_CODE_200__ to register a fully-mocked service in a __INLINE_CODE_201__, then __INLINE_CODE_202__ to pull it back out already typed as __INLINE_CODE_203__. __INLINE_CODE_204__ / __INLINE_CODE_205__ are your own (optional) peers — the helper imports neither:
__CODE_BLOCK_18__React (Testing Library)
React has no DI container, so there's no __INLINE_CODE_206__ helper — the recipe is: spy the classes you own (services, stores, API clients, hook deps), then pass the spy into a Context provider or hook. The spy is a plain object of spied functions, so it drops straight into __INLINE_CODE_207__:
__CODE_BLOCK_19__Vue / Pinia
__INLINE_CODE_208__ returns a __INLINE_CODE_209__ map you can spread into __INLINE_CODE_210__' __INLINE_CODE_211__; for a class-based Pinia store, spy it directly:
__CODE_BLOCK_20__Svelte
Svelte has no class-based DI, so it's a recipe: keep your logic in plain class-based services/stores, spy the class, and hand the spy to the component the same way it receives the real one (props, context, or a mocked module):
__CODE_BLOCK_21__Angular
__INLINE_CODE_212__ is the shorthand for providing an auto-spy in a __INLINE_CODE_213__:
__CODE_BLOCK_22__The spies are change-detection agnostic, so they work in both zoneless and zone.js Angular projects — nothing here touches __INLINE_CODE_214__ or change detection. You only need the usual Vitest + Angular wiring: __INLINE_CODE_215__ plus a TestBed setup file (e.g. __INLINE_CODE_216__'s __INLINE_CODE_217__).
Lazy by default. __INLINE_CODE_218__ builds each method spy on first access (__INLINE_CODE_219__), since Angular tests typically spy a wide service but call only a few of its methods — roughly 4× faster spy assembly (≈8× on a 20-method service). Behaviour is unchanged; pass __INLINE_CODE_220__ to build every spy eagerly.
Signal / readonly property mocking (bonus)
__CODE_BLOCK_23__Utilities
Beyond the spy factories, the package ships a set of small standalone helpers. Each one is a single-purpose utility you can pick up independently — they all ride on the same core:
| Utility | Entry point | What it's for |
|---|---|---|
| __INLINE_CODE_221__ / __INLINE_CODE_222__ | __INLINE_CODE_223__, __INLINE_CODE_224__ | Pull a provided spy out of the DI container, already typed as __INLINE_CODE_225__ — no casting |
| __INLINE_CODE_226__ | __INLINE_CODE_227__, __INLINE_CODE_228__, __INLINE_CODE_229__ | One-liner __INLINE_CODE_230__ (or Vue __INLINE_CODE_231__) that builds the spy for you |
| __INLINE_CODE_232__ | core | A single standalone function spy with the full helper set (__INLINE_CODE_233__, __INLINE_CODE_234__, __INLINE_CODE_235__, …) — no class needed |
| __INLINE_CODE_236__ | core | Proxy-based spy from a type/interface alone (details) |
| __INLINE_CODE_237__ | __INLINE_CODE_238__ | Build a fake __INLINE_CODE_239__ emitting a precise sequence of values / errors / completion |
| __INLINE_CODE_240__ / __INLINE_CODE_241__ / … | __INLINE_CODE_242__ | Silent typed spies over the global __INLINE_CODE_243__, installed on import (details) |
| __INLINE_CODE_244__ | __INLINE_CODE_245__ | Overwrite a __INLINE_CODE_246__ property (incl. Angular signals) with a static value |
| __INLINE_CODE_247__ | __INLINE_CODE_248__ | Same, but backed by a dynamic getter |
| __INLINE_CODE_249__ | __INLINE_CODE_250__ | Redefine a property with spied __INLINE_CODE_251__ + __INLINE_CODE_252__ |
| __INLINE_CODE_253__ | core | The __INLINE_CODE_254__ argument-mismatch reporter — swap it to customize failure output |
A taste of the DI pair — provide the spy, inject it back fully typed:
__CODE_BLOCK_24__And a standalone function spy, when there's no class or interface at all:
__CODE_BLOCK_25__Console spies — __INLINE_CODE_255__
Importing the entry replaces __INLINE_CODE_256__ / __INLINE_CODE_257__ / __INLINE_CODE_258__ / __INLINE_CODE_259__ / __INLINE_CODE_260__ / __INLINE_CODE_261__ / __INLINE_CODE_262__ / __INLINE_CODE_263__ with silent, fully-typed spies and exports each one ready to assert — no __INLINE_CODE_264__ boilerplate in every suite, no log output polluting the test run:
__CODE_BLOCK_26__Housekeeping: __INLINE_CODE_265__ clears the recorded calls between tests (Vitest's __INLINE_CODE_266__ already does that automatically), __INLINE_CODE_267__ puts the original methods back, and __INLINE_CODE_268__ re-installs after a restore.
The spies use the registered __INLINE_CODE_269__ — import your runtime entry (__INLINE_CODE_270__, __INLINE_CODE_271__) before __INLINE_CODE_272__ and the console spies are driven by that runner's mocks; with no prior runtime entry the default Vitest adapter is used.
Prefer a fully detached fake instead of touching the real global? __INLINE_CODE_273__ gives you a typed, in-memory console to inject into code that takes a logger:
__CODE_BLOCK_27__API reference
| Export | Description |
|---|---|
| __INLINE_CODE_274__ | Build a fully-typed __INLINE_CODE_275__ from a class |
| __INLINE_CODE_276__ | Build a __INLINE_CODE_277__ from a type/interface alone (Proxy, no class) |
| __INLINE_CODE_278__ | Build a recursive auto-mock — __INLINE_CODE_279__ chains without seeding |
| __INLINE_CODE_280__ / __INLINE_CODE_281__ | Reset every spy in an auto-spy at once — __INLINE_CODE_282__ also reverts return-value config (__INLINE_CODE_283__ and a bare __INLINE_CODE_284__); __INLINE_CODE_285__ keeps it |
| __INLINE_CODE_286__ | Angular / NestJS __INLINE_CODE_287__ shorthand |
| __INLINE_CODE_288__ | Vue __INLINE_CODE_289__ for __INLINE_CODE_290__ |
| __INLINE_CODE_291__ (Angular) / __INLINE_CODE_292__ (NestJS) | Inject typed as __INLINE_CODE_293__ |
| __INLINE_CODE_294__ | A single standalone function spy with all helpers |
| __INLINE_CODE_295__ | Build an Observable from value configs |
| __INLINE_CODE_296__ / __INLINE_CODE_297__ / __INLINE_CODE_298__ | Mock readonly / accessor / signal props |
| __INLINE_CODE_299__ … __INLINE_CODE_300__ (__INLINE_CODE_301__) | Silent typed spies replacing the global __INLINE_CODE_302__ methods on import |
| __INLINE_CODE_303__ / __INLINE_CODE_304__ / __INLINE_CODE_305__ | Install / clear / undo the console spies |
| __INLINE_CODE_306__ | The __INLINE_CODE_307__ argument-mismatch error helper |
Spied sync method: __INLINE_CODE_308__, __INLINE_CODE_309__, __INLINE_CODE_310__ — __INLINE_CODE_311__ also matches asymmetric matchers (__INLINE_CODE_312__, __INLINE_CODE_313__)
Spied Promise method: __INLINE_CODE_314__, __INLINE_CODE_315__, __INLINE_CODE_316__
Spied Observable method / property: __INLINE_CODE_317__, __INLINE_CODE_318__, __INLINE_CODE_319__, __INLINE_CODE_320__, __INLINE_CODE_321__, __INLINE_CODE_322__, __INLINE_CODE_323__
Config (__INLINE_CODE_324__): __INLINE_CODE_325__, __INLINE_CODE_326__, __INLINE_CODE_327__, __INLINE_CODE_328__, __INLINE_CODE_329__ (discover every getter/setter), __INLINE_CODE_330__ (materialize method spies on first access — cheaper for wide classes; the __INLINE_CODE_331__ default on Angular)
__INLINE_CODE_332__ (for __INLINE_CODE_333__): __INLINE_CODE_334__ | __INLINE_CODE_335__ | __INLINE_CODE_336__.
FAQ & troubleshooting
"I get __INLINE_CODE_337__ / observable helpers are missing." Import the rxjs layer once (e.g. in your test setup): __INLINE_CODE_338__. Without it, requesting an observable spy throws a hint pointing you here.
"My method isn't on the spy." Auto-discovery only sees prototype methods. Arrow-function class fields (__INLINE_CODE_339__) and plain properties aren't included — see How it works. List getters/setters via __INLINE_CODE_340__ / __INLINE_CODE_341__.
"Does it construct my class? Will the constructor's side effects run?" No. __INLINE_CODE_342__ reads the prototype and never __INLINE_CODE_343__s the class, so constructors (and their HTTP/DB/__INLINE_CODE_344__ side effects) never run.
"I only have an interface/type, not a class." Use __INLINE_CODE_345__ — it builds the spy lazily from the type via a __INLINE_CODE_346__, no runtime class needed.
"Can I use it without TypeScript?" Yes — the runtime works in plain JS; you just lose the compile-time __INLINE_CODE_347__ typing.
"Native mock methods differ between runners." Only the auto-spy helpers are normalised. Native APIs stay the runner's own (__INLINE_CODE_348__ on Vitest/Bun, __INLINE_CODE_349__ on __INLINE_CODE_350__).
Versioning
This package follows Semantic Versioning. Breaking changes to the public API land only in major releases; see the Changelog for what changed in each version. Releases are automated from Conventional Commits (see Contributing).
Contributing
Contributions are welcome! Please read CONTRIBUTING.md and the Code of Conduct. In short:
__CODE_BLOCK_28__Tip: develop against __INLINE_CODE_351__ — it skips the v8 coverage instrumentation that __INLINE_CODE_352__ adds, so the feedback loop is noticeably faster. Run __INLINE_CODE_353__ before pushing to confirm the 100% thresholds.
Releases are automated: merging a PR into __INLINE_CODE_354__ bumps the version from the Conventional Commit types and publishes to npm — see CONTRIBUTING.md → Releasing.
If this package saved you time, a on GitHub helps others find it.
Acknowledgements
API and ergonomics are modelled on Shai Reznik's __INLINE_CODE_355__ — __INLINE_CODE_356__ is its Vitest-era successor with the same surface, so migrations are (mostly) a find-and-replace. Thanks to the Vitest, Bun, RxJS and Angular communities whose tooling this builds on.
License
Get in touch: asdalexey.github.io