vitest-auto-spy

July 4, 2026 · View on GitHub

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 / nextWith / calledWith). 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.

npm version npm downloads CI minzipped size types coverage license

Vitest Bun node:test runtime deps

📦 npm · 🐙 GitHub · 🔖 Changelog


  • 🧪 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 MockAdapter core — Vitest, Bun and node:test, identical API on each
  • 🧩 Framework recipes: Angular, NestJS, React, Vue/Pinia and Svelte
  • 🎯 Return-type-aware helpers — sync, Promise, and Observable all get the right API
  • 🔀 calledWith / mustBeCalledWith argument dispatch
  • 📡 First-class RxJS Observable spying (nextWith, nextWithValues, throwWith, …)
  • ⚙️ Getter / setter spies via accessorSpies
  • 🧰 DI & mocking utilities — provideAutoSpy / injectSpy (Angular, NestJS, Vue), createFunctionSpy, mockReadonlyProp for signals
  • 🔇 Console spies — import { consoleInfoSpy } from 'vitest-auto-spy/console' silences console and asserts its calls
  • 🟢 100% test coverage, zero runtime dependencies (in-tree arg serializer, no javascript-stringify)

Table of contents

Install

npm i -D vitest-auto-spy

Requirements

ToolMinimum
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.

PeerNeeded forOptional?
vitestthe default runnerno
rxjsvitest-auto-spy/rxjs observable spies (and Spy<T> type-checking)yes
@angular/corevitest-auto-spy/angular helpersyes

Availability

All entry points are published. The Vitest / Bun / node:test runtimes, 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 pointStatus
vitest-auto-spy · vitest-auto-spy/rxjs · vitest-auto-spy/angularPublished
vitest-auto-spy/bun · vitest-auto-spy/nodePublished
vitest-auto-spy/nestjs · /react · /vue · /svelte · /consolePublished

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; PromiseresolveWith / rejectWith; ObservablenextWith / 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 as jest-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:

ImportProvidesPulls inStatus
vitest-auto-spycreateSpyFromClass, createAutoMock, createFunctionSpy, sync + promise + accessor spies, errorHandler, typesvitest
vitest-auto-spy/rxjsobservable spies (nextWith, nextWithValues, observablePropsToSpyOn, …) + createObservableWithValuesrxjs
vitest-auto-spy/angularprovideAutoSpy, injectSpy, mockReadonlyProp*, mockAccessorsProp@angular/core
vitest-auto-spy/bunthe same core, driven by Bun's bun:test mocksbun:test
vitest-auto-spy/nodethe same core, driven by node:test's mock.fn()node:test
vitest-auto-spy/nestjsprovideAutoSpy, injectSpy for Test.createTestingModule— (your @nestjs/*)
vitest-auto-spy/reactthe core, with a natural import for React Testing Library suites— (your react)
vitest-auto-spy/vueprovideAutoSpy for global.provide + Pinia store spying— (your vue/pinia)
vitest-auto-spy/sveltethe core, with a natural import for Svelte suites— (your svelte)
vitest-auto-spy/consoleconsoleInfoSpy & friends — silent typed spies over the global console, installed on importvitest

✅ all entry points published (see Availability).

The framework subpaths import nothing from their framework — the helpers are structural, so @nestjs/*, react, vue/pinia and svelte stay 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 — mockReturnValue on Vitest/Bun, spy.method.mock.mockImplementation on node:test. Each entry registers its adapter on import, so import the one matching your runner.

Using an observable spy (observablePropsToSpyOn, nextWith, …) without importing vitest-auto-spy/rxjs throws 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 keep rxjs available 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 vitest directly — vi.fn() / vi.spyOn() sit behind a MockAdapter that the vitest-auto-spy entry registers by default, so it stays zero-config. This is the groundwork for running the exact same core on other Vitest-compatible runners.

Comparison

LibraryReads a class?Return-type-aware helpers?RuntimesWe win on
vitest-auto-spyVitest (Bun · node:test next)
jest-auto-spiesJest onlyVitest/Bun/Node successor, same API — direct migration path
vitest-mock-extended❌ (Proxy)VitestReturn-type ergonomics and reading a real class (we also ship a Proxy mode: createAutoMock)
@golevelup/ts-vitestpartialVitestTyped Promise/Observable helpers + explicit class→spy + mustBeCalledWith
sinon❌ (manual)AnyAuto-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).

jest-auto-spiesvitest-auto-spyStatus
createSpyFromClasscreateSpyFromClass✅ identical
provideAutoSpyprovideAutoSpy✅ identical
calledWith / mustBeCalledWithsame✅ identical
resolveWith / rejectWith / resolveWithPerCallsame✅ identical
nextWith / nextOneTimeWith / nextWithValues / nextWithPerCallsame✅ identical
throwWith / complete / returnSubjectsame✅ identical
accessorSpies.getters/setterssame✅ identical
createObservableWithValuessame✅ identical
underlying mockjest.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$'], // Observable *properties*
  gettersToSpyOn: ['userName'],
  settersToSpyOn: ['userName'],
});

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()).

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:

UtilityEntry pointWhat it's for
injectSpy(token) / injectSpy(moduleRef, token)/angular, /nestjsPull a provided spy out of the DI container, already typed as Spy<T> — no casting
provideAutoSpy(Class, config?)/angular, /nestjs, /vueOne-liner { provide, useValue } (or Vue global.provide) that builds the spy for you
createFunctionSpy(name)coreA single standalone function spy with the full helper set (calledWith, resolveWith, nextWith, …) — no class needed
createAutoMock<T>(overrides?)coreProxy-based spy from a type/interface alone (details)
createObservableWithValues(configs, opts?)/rxjsBuild a fake Observable emitting a precise sequence of values / errors / completion
consoleInfoSpy / consoleWarnSpy / …/consoleSilent typed spies over the global console, installed on import (details)
mockReadonlyProp(obj, prop, value)/angularOverwrite a readonly property (incl. Angular signals) with a static value
mockReadonlyPropGetter(obj, prop, getter)/angularSame, but backed by a dynamic getter
mockAccessorsProp(obj, prop)/angularRedefine a property with spied get + set
errorHandlercoreThe 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

ExportDescription
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)
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 / mockAccessorsPropMock readonly / accessor / signal props
consoleDebugSpyconsoleWarnSpy (/console)Silent typed spies replacing the global console methods on import
installConsoleSpies() / resetConsoleSpies() / restoreConsole()Install / clear / undo the console spies
errorHandlerThe mustBeCalledWith argument-mismatch error helper

Spied sync method: mockReturnValue, calledWith(...), mustBeCalledWith(...)

Spied Promise method: resolveWith, rejectWith, resolveWithPerCall

Spied Observable method / property: nextWith, nextOneTimeWith, nextWithValues, nextWithPerCall, throwWith, complete, returnSubject

Config (ClassSpyConfiguration): methodsToSpyOn, observablePropsToSpyOn, gettersToSpyOn, settersToSpyOn

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
npm run test:coverage   # 100% thresholds enforced
npm run build

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-spiesvitest-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

MIT © Alexey Popov

Get in touch: asdalexey.github.io