Skip to content

Installation

bash
npm i -D vitest-auto-spy

The plural name is an alias

vitest-auto-spies is a thin alias package that re-exports this one, entry point for entry point — a typo installs the same code. Prefer the singular name; the alias is generated from it and only ever follows it.

Peer dependencies are all 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 — >=7, no upper bound (rxjs 8 too)yes
@angular/corevitest-auto-spy/angular and vitest-auto-spy/bun-angular helpersyes
ToolMinimum
Node.js≥ 18 for the library — in practice, whatever your runner needs
Vitest≥ 2.1
Bun≥ 1.4 for vitest-auto-spy/bun-angular; any recent Bun for /bun
TypeScript≥ 4.7 for the typed helpers (plain JS works too, just untyped)

Vitest ≥ 2.1 because the typed spy.method.mock.settledResults surface is Vitest's own Mock type, and @vitest/spy only grew settledResults in 2.0 — 2.1 is where the 2.x line actually sits. The runtime helpers themselves still run on older Vitest (the library polyfills settledResults for bun:test and node:test regardless), but the types no longer line up there, so the range stops claiming it.

Node ≥ 18 is the library's own floor and it holds: the published output is ES2022 and every entry runs on 18. What moves the real minimum is the runner. Vitest 4 cannot start on Node 18 at all — it pulls Vite 7, which calls crypto.hash (added in Node 20.12), and the run dies with TypeError: crypto.hash is not a function before a single spec loads. Vitest 4 declares ^20.0.0 || ^22.0.0 || >=24.0.0; Vite 7 is stricter at ^20.19.0 || >=22.12.0. On an older Vitest (≤ 3, Vite 5/6) Node 18 is fine. Which version to actually run — and what it costs — is measured in Performance → Which Node version.

Ships ESM with bundled .d.ts types. Two subpaths additionally ship a CommonJS build — vitest-auto-spy/node (a node --test suite written in CJS) and vitest-auto-spy/eslint-plugin (loaded by a CommonJS eslint.config.cjs). Everything else is ESM-only, because a require() of it could never have worked: Vitest itself refuses to be required (Vitest cannot be imported in a CommonJS module using require()), so every Vitest-backed entry threw on the first line of its own .cjs. Test runners load ESM natively, so nothing is lost — and dropping the unreachable output cut the published package roughly in half.

Entry points

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 in
vitest-auto-spycreateSpyFromClass, createAutoMock, mockDeep, createMock, createFunctionSpy, the mock*Prop helpers, the observable assertions, the type bridges, errorHandler, typesvitest
vitest-auto-spy/bunthe same core, driven by Bun's bun:test mocksbun:test
vitest-auto-spy/bun-angularAngular's TestBed under bun test — DOM, JIT templateUrl resolution and a zoneless environment from one preload, plus the core and the Angular helpersbun:test, @angular/core
vitest-auto-spy/nodethe same core, driven by node:test's mock.fn()node:test
vitest-auto-spy/rxjsobservable spies (nextWith, nextWithValues, observablePropsToSpyOn, …) + createObservableWithValuesrxjs
vitest-auto-spy/angularprovideAutoSpy, injectSpy, renderShallow, createWithAutoSpies, stable/flushEffects, signal matchers, TestBed diagnostics, the mock*Prop helpers@angular/core
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/consoleconsole spies — silent typed spies over the global consolevitest
vitest-auto-spy/setupsetupAutoSpy() and setupFakeTimers()vitest
vitest-auto-spy/eslint-pluginthe lint rules that steer a suite onto these helpers— (your eslint)

Each entry registers its mock adapter on import, so import the one matching your test runner — mixing vitest-auto-spy into a bun test run leaves the wrong adapter installed.

Wiring it up

Vitest

Zero-config: import { createSpyFromClass } from 'vitest-auto-spy' in a spec is enough. A setup file is only needed for things that are global by nature — the rxjs layer and run hygiene:

ts
// vitest.setup.ts
import 'vitest-auto-spy/rxjs'; // once — enables observable spies everywhere
import { setupAutoSpy } from 'vitest-auto-spy/setup';

setupAutoSpy();
ts
// vitest.config.ts
export default defineConfig({
  test: {
    setupFiles: ['./vitest.setup.ts'],
  },
});

setupAutoSpy() matters most when the suite shares one environment (isolate: false), where an un-restored property patch outlives the file that made it. See Test-run hygiene.

Bun

ts
// user.test.ts
import { describe, expect, it } from 'bun:test';
import { createSpyFromClass } from 'vitest-auto-spy/bun';
bash
bun test

For the equivalent of a Vitest setup file, use a preload:

toml
# bunfig.toml
[test]
preload = ["./bun-setup.ts"]

Angular under bun test has its own entry and its own preload — see Angular on Bun. Bun 1.4's --isolate, --parallel, --shard, --changed and --timings all work unchanged; Bun covers what each one means for your spies.

node:test

ts
// user.test.ts
import { describe, it } from 'node:test';
import { createSpyFromClass } from 'vitest-auto-spy/node';
bash
node --test

node:test has no expect; pair it with node:assert (or any assertion library) — the spy surface is the same either way.

TypeScript

The typed helpers need nothing beyond a normal setup, with one thing worth knowing: the Spy<T> type surface references rxjs types even when you never import the rxjs layer. Keep rxjs installed for type-checking — it is normally already a devDependency — and none of it reaches your runtime bundle.

jsonc
{
  "compilerOptions": {
    // "bundler" or "node16"/"nodenext" — anything that understands `exports` subpaths
    "moduleResolution": "bundler",
  },
}

Spy<T> is a mapped type: it drops #private and private members, so it is not assignable to T. Declare the variable as Spy<T> rather than as T, or bridge the two with asInstance / asSpy.

Released under the MIT License.