Installation
npm i -D vitest-auto-spyThe 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.
| Peer | Needed for | Optional? |
|---|---|---|
vitest | the default runner | no |
rxjs | vitest-auto-spy/rxjs observable spies — >=7, no upper bound (rxjs 8 too) | yes |
@angular/core | vitest-auto-spy/angular and vitest-auto-spy/bun-angular helpers | yes |
| Tool | Minimum |
|---|---|
| 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:
| Import | Provides | Pulls in |
|---|---|---|
vitest-auto-spy | createSpyFromClass, createAutoMock, mockDeep, createMock, createFunctionSpy, the mock*Prop helpers, the observable assertions, the type bridges, errorHandler, types | vitest |
vitest-auto-spy/bun | the same core, driven by Bun's bun:test mocks | bun:test |
vitest-auto-spy/bun-angular | Angular's TestBed under bun test — DOM, JIT templateUrl resolution and a zoneless environment from one preload, plus the core and the Angular helpers | bun:test, @angular/core |
vitest-auto-spy/node | the same core, driven by node:test's mock.fn() | node:test |
vitest-auto-spy/rxjs | observable spies (nextWith, nextWithValues, observablePropsToSpyOn, …) + createObservableWithValues | rxjs |
vitest-auto-spy/angular | provideAutoSpy, injectSpy, renderShallow, createWithAutoSpies, stable/flushEffects, signal matchers, TestBed diagnostics, the mock*Prop helpers | @angular/core |
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 | console spies — silent typed spies over the global console | vitest |
vitest-auto-spy/setup | setupAutoSpy() and setupFakeTimers() | vitest |
vitest-auto-spy/eslint-plugin | the 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:
// vitest.setup.ts
import 'vitest-auto-spy/rxjs'; // once — enables observable spies everywhere
import { setupAutoSpy } from 'vitest-auto-spy/setup';
setupAutoSpy();// 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
// user.test.ts
import { describe, expect, it } from 'bun:test';
import { createSpyFromClass } from 'vitest-auto-spy/bun';bun testFor the equivalent of a Vitest setup file, use a preload:
# 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
// user.test.ts
import { describe, it } from 'node:test';
import { createSpyFromClass } from 'vitest-auto-spy/node';node --testnode: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.
{
"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.