vitest-auto-spy 4.0.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +83 -3
- package/README.md +233 -6
- package/dist/angular.js +421 -31
- package/dist/bun-angular.js +5 -5
- package/dist/bun.js +3 -3
- package/dist/{chunk-XJ3UMJN3.js → chunk-2ZG27FVL.js} +1 -1
- package/dist/{chunk-N4PT3JYE.js → chunk-4KCYFAJX.js} +1 -1
- package/dist/{chunk-3QJUFL2N.js → chunk-4KK3ES4P.js} +49 -8
- package/dist/{chunk-M3XZL5NF.js → chunk-5URFSYDT.js} +2 -2
- package/dist/{chunk-6EGUFU73.js → chunk-KEKKEIRG.js} +1 -1
- package/dist/chunk-WQZ2RKS7.js +397 -0
- package/dist/chunk-YHDBRBGU.js +5 -0
- package/dist/cli.js +546 -9
- package/dist/console.js +4 -2
- package/dist/dom-stubs.js +378 -7
- package/dist/eslint-plugin.js +2 -1658
- package/dist/index.js +421 -31
- package/dist/jasmine.js +5 -4
- package/dist/nestjs.js +6 -5
- package/dist/node.cjs +48 -7
- package/dist/node.js +3 -3
- package/dist/perf-reporter.js +52 -0
- package/dist/react.js +5 -4
- package/dist/rxjs.js +1 -3
- package/dist/setup.d.ts +19 -1
- package/dist/setup.js +11 -3
- package/dist/svelte.js +5 -4
- package/dist/vue.js +6 -5
- package/package.json +9 -3
- package/skills/vitest-auto-spy/SKILL.md +67 -61
- package/dist/chunk-MG5EAMZR.js +0 -36
package/AGENTS.md
CHANGED
|
@@ -261,9 +261,10 @@ xhr.send.mockImplementation(() => respond(asInstance(xhr)));
|
|
|
261
261
|
|
|
262
262
|
### Cost, so it stops being a question
|
|
263
263
|
|
|
264
|
-
Building a spy is not a thing to optimise. On a ten-method class
|
|
265
|
-
`createSpyFromClass`
|
|
266
|
-
|
|
264
|
+
Building a spy is not a thing to optimise. On a ten-method class, Node 24, `npm run bench`:
|
|
265
|
+
`createSpyFromClass` plus two methods called ~2.3 µs, plus all ten ~6 µs, `createAutoMock` plus four
|
|
266
|
+
members ~1.7 µs, a `calledWith` lookup ~0.2 µs — five providers across two thousand tests is a
|
|
267
|
+
hundredth of a second. Call the factory in `beforeEach` and look at
|
|
267
268
|
`TestBed` instead. The only two settings that cost: `{ lazySpies: false }` gives up the laziness
|
|
268
269
|
`provideAutoSpy` defaults to, and `autoSpyAccessors: true` walks the prototype chain uncached on
|
|
269
270
|
every call — name the accessors instead.
|
|
@@ -1053,6 +1054,48 @@ is therefore remembered and put back, in `beforeEach`, but only when it has gone
|
|
|
1053
1054
|
returns how many went), `restoreLongLivedImplementations()` is that repair (it returns how many it
|
|
1054
1055
|
put back) and `getMockRegistrySize()` reports what is left.
|
|
1055
1056
|
|
|
1057
|
+
One entry is never pruned, and it is not one of yours: the Vitest adapter registers a single
|
|
1058
|
+
`vi.fn()` whose `mockClear` sweeps this library's own spies. Dropping it would turn
|
|
1059
|
+
`vi.clearAllMocks()` into a silent no-op for every double in the run, so it carries a mark the
|
|
1060
|
+
pruner skips. Why the library needs a mock of its own for that is the next section.
|
|
1061
|
+
|
|
1062
|
+
### What a method spy is, and the one thing that differs from `vi.fn()`
|
|
1063
|
+
|
|
1064
|
+
A method spy is **not** a `vi.fn()`. It is this library's own mock function: one shared prototype
|
|
1065
|
+
carrying the whole `Mock` surface, call state allocated on the first call rather than at creation,
|
|
1066
|
+
and no entry in any global registry. `vi.fn()` assigns some twenty-five closures as own properties
|
|
1067
|
+
of every mock it makes and allocates six arrays up front — per method, on every double a spec
|
|
1068
|
+
builds — and that cost is most of what a wide service used to pay for methods no test touches.
|
|
1069
|
+
|
|
1070
|
+
Everything a spec can observe is the same, and the suite pins it by putting the two side by side:
|
|
1071
|
+
|
|
1072
|
+
- `vi.isMockFunction(spy.load)`, `expect(spy.load).toHaveBeenCalledWith(…)`, `toHaveReturned`,
|
|
1073
|
+
`toHaveResolved`, `toHaveBeenNthCalledWith` — every matcher reads `_isMockFunction`, `mock.*` and
|
|
1074
|
+
`getMockName()`, all of which the spy implements with the runner's semantics.
|
|
1075
|
+
- `spy.load.mock.calls` / `.results` / `.settledResults` / `.instances` / `.contexts` /
|
|
1076
|
+
`.invocationCallOrder` / `.lastCall`, `mockReturnValue`, `mockResolvedValue`,
|
|
1077
|
+
`mockImplementation(Once)`, `withImplementation`, `mockClear` / `mockReset` / `mockRestore`,
|
|
1078
|
+
`mockName`, `using`.
|
|
1079
|
+
- `vi.clearAllMocks()`, `vi.resetAllMocks()`, and the `clearMocks` / `mockReset` config keys, which
|
|
1080
|
+
Vitest applies through those two functions.
|
|
1081
|
+
|
|
1082
|
+
**The one difference.** `mock.invocationCallOrder` counts on this library's own scale, so
|
|
1083
|
+
`expect(a).toHaveBeenCalledBefore(b)` is exact between two auto-spies and meaningless between an
|
|
1084
|
+
auto-spy and a hand-written `vi.fn()` — the two counters never met. Nothing else in the library or in
|
|
1085
|
+
Vitest reads that field. If a suite needs the comparison, switch the whole run back to the runner's
|
|
1086
|
+
factory:
|
|
1087
|
+
|
|
1088
|
+
```ts
|
|
1089
|
+
// vitest.setup.ts
|
|
1090
|
+
import { setSpyEngine } from 'vitest-auto-spy/setup';
|
|
1091
|
+
|
|
1092
|
+
setSpyEngine('runner'); // every double built afterwards is `vi.fn()` per method, as before 4.1
|
|
1093
|
+
```
|
|
1094
|
+
|
|
1095
|
+
`getSpyEngine()` reports the current one, `'auto-spy'` is the default, and doubles already built keep
|
|
1096
|
+
the engine they were built with. The switch is Vitest-only: on Bun and `node:test` the runner's own
|
|
1097
|
+
matchers recognise only the runner's own mocks, so those adapters keep using them.
|
|
1098
|
+
|
|
1056
1099
|
Two more switches, both about the environment rather than the spies:
|
|
1057
1100
|
|
|
1058
1101
|
```ts
|
|
@@ -2072,6 +2115,14 @@ stubbed a since-renamed method was passing over nothing and will now fail. Full
|
|
|
2072
2115
|
`fesm2022/testing-library-angular-vitest-utils.mjs` lines 14 and 18 at 19.4.2. Re-`npm pack` and
|
|
2073
2116
|
re-read before restating them against a new version; the "Where the numbers come from" box records
|
|
2074
2117
|
each re-verification date rather than replacing the previous one.
|
|
2118
|
+
- **Head-to-head benchmark numbers are quoted from the measurement, not from memory.** `npm run
|
|
2119
|
+
bench:vs` and `npm run bench:suite` (see `CONTRIBUTING.md` for setup) back the claims in
|
|
2120
|
+
`core/performance.md`; point at that page rather than retyping a figure here, so it lives in one
|
|
2121
|
+
place. Micro-benchmark ratios do not transfer to suite scale: double construction is on the order
|
|
2122
|
+
of 1% of a test's cost, and an advantage that looks like several times faster per double can be
|
|
2123
|
+
gone, or reversed, once a whole suite runs. Suite-scale wall-clock is noisy between invocations;
|
|
2124
|
+
peak RSS reproduces far more tightly and is the safer claim. Never state a magnitude the source
|
|
2125
|
+
marks as not established.
|
|
2075
2126
|
|
|
2076
2127
|
`node:test` keeps every `mock.fn()` in one process-wide `MockTracker` for the life of the process.
|
|
2077
2128
|
On a long suite that is real memory — 20 000 spies of a 10-method class retained 124.5 MB on
|
|
@@ -2521,6 +2572,35 @@ worth one run after any large edit to a test suite — especially after a codemo
|
|
|
2521
2572
|
the eaten glob below came from. Full reference:
|
|
2522
2573
|
<https://asdalexey.github.io/vitest-auto-spy/utilities/cli>.
|
|
2523
2574
|
|
|
2575
|
+
### If you were asked why a suite is slow
|
|
2576
|
+
|
|
2577
|
+
```bash
|
|
2578
|
+
npx vitest-auto-spy perf # runs the suite once and reports; always exits 0
|
|
2579
|
+
npx vitest-auto-spy perf src/some/dir # a path is passed through to Vitest as a file filter
|
|
2580
|
+
```
|
|
2581
|
+
|
|
2582
|
+
It reads Vitest's own per-file phase timings through `TestModule.diagnostic()` — nothing here
|
|
2583
|
+
parses terminal output — and reports six phases: `environment`, `import`, `tests`, `setup`,
|
|
2584
|
+
`prepare` (all measured per file) and `transform` (measured once for the whole run). **The phase
|
|
2585
|
+
totals are CPU time summed across every worker, not wall clock** — a report showing `20.29s of CPU
|
|
2586
|
+
time` under `986ms wall clock` is not a bug, it is the work spread over workers.
|
|
2587
|
+
|
|
2588
|
+
When `environment` dominates, the `perf-environment` finding names spec files that could run under
|
|
2589
|
+
the `node` environment instead — but only the ones it could *prove* reach no DOM: the spec, its
|
|
2590
|
+
configured setup files, and every repository module any of them imports were all read, none
|
|
2591
|
+
mentions a DOM name, and every package they import is on a short allowlist (`vitest`, `rxjs`,
|
|
2592
|
+
`date-fns`, `lodash`, `zod`, …). Anything it could not resolve is reported **undecided**, never
|
|
2593
|
+
assumed safe — do not treat an undecided file as a candidate, and do not add packages to that list
|
|
2594
|
+
yourself; a false positive breaks someone's suite on `document is not defined`. When `import`
|
|
2595
|
+
dominates, `perf-import` names specs that reach their subject through a barrel (`index`/`public-api`
|
|
2596
|
+
re-exporting a whole directory). When `environment` + `setup` + `prepare` together dominate,
|
|
2597
|
+
`perf-isolation` suggests `test.isolate: false` and links to this package's own memory measurements
|
|
2598
|
+
of that trade rather than restating them — read that page before recommending the flag, since it
|
|
2599
|
+
raises peak memory.
|
|
2600
|
+
|
|
2601
|
+
`--json <path>` re-analyses a report an earlier `--out <path>` run wrote, instead of running Vitest
|
|
2602
|
+
again. Full reference: <https://asdalexey.github.io/vitest-auto-spy/utilities/cli>.
|
|
2603
|
+
|
|
2524
2604
|
### Migrating a suite off `jest-auto-spies` — run the codemod, then verify it
|
|
2525
2605
|
|
|
2526
2606
|
```bash
|
package/README.md
CHANGED
|
@@ -11,17 +11,19 @@ skip the class entirely and mock straight from a **type or interface** with `cre
|
|
|
11
11
|
recursive `mockDeep<T>()`. Runs on **Vitest**, **Bun** (`bun:test`) and **`node:test`** behind one
|
|
12
12
|
identical API, with **RxJS** spies and **Angular / NestJS / React / Vue·Pinia / Svelte** recipes
|
|
13
13
|
([availability](#availability)). A drop-in replacement for
|
|
14
|
-
[`jest-auto-spies`](https://www.npmjs.com/package/jest-auto-spies) — same API
|
|
14
|
+
[`jest-auto-spies`](https://www.npmjs.com/package/jest-auto-spies) — same API, and roughly 1.5x
|
|
15
|
+
faster at suite scale ([benchmarks](#benchmarks)) — and for
|
|
15
16
|
[`jasmine-auto-spies`](https://www.npmjs.com/package/jasmine-auto-spies) through
|
|
16
17
|
[`vitest-auto-spy/jasmine`](#migrating-from-jasmine-auto-spies), which keeps `.and`, `.calls` and
|
|
17
18
|
`.withArgs` working while you land the suite green.
|
|
18
19
|
|
|
19
20
|
[](https://www.npmjs.com/package/vitest-auto-spy)
|
|
21
|
+
[](https://www.npmjs.com/package/vitest-auto-spy)
|
|
20
22
|
[](https://www.npmjs.com/package/vitest-auto-spy)
|
|
21
23
|
[](https://www.npmjs.com/package/vitest-auto-spy)
|
|
22
24
|
[](https://www.npmjs.com/package/vitest-auto-spy)
|
|
23
25
|
[](https://github.com/ASDAlexey/vitest-auto-spy/actions/workflows/ci.yml)
|
|
24
|
-
[](#install)
|
|
25
27
|
[](https://www.npmjs.com/package/vitest-auto-spy)
|
|
26
28
|
[](https://github.com/ASDAlexey/vitest-auto-spy/actions/workflows/ci.yml)
|
|
27
29
|
[](./LICENSE)
|
|
@@ -75,7 +77,8 @@ identical API, with **RxJS** spies and **Angular / NestJS / React / Vue·Pinia /
|
|
|
75
77
|
- 🚚 A migration you can verify — `vitest-auto-spy/diagnostics`: `compareTestRuns` on the two JSON reports, `summarizeTestRun` / `formatTestRunComparison` to read the answer, `diffByField` for the assertion the reporter collapses
|
|
76
78
|
- 📏 Lint rules and one-line test-run hygiene — nineteen rules in `vitest-auto-spy/eslint-plugin` (three `--fix`, seven suggestions, four of them for a suite mid-migration off jasmine), `setupAutoSpy()`
|
|
77
79
|
- 🩺 [Editor diagnostics](#editor-diagnostics--webstorm--vs-code) — the same anti-patterns underlined while you type: native ESLint inspections in **WebStorm** and the other JetBrains IDEs, the ESLint extension in **VS Code**, no extra plugin either way
|
|
78
|
-
- 🔎 [`npx vitest-auto-spy doctor`](#the-cli--doctor-codemod-and-init) — suite-level defects **that never fail a run**: a `tsconfig` `include` matching no file, a production module importing a spec, a `@jest-environment` pragma the runner never reads, config left behind for a runner that is gone. Read-only, no config, exits 1 in CI
|
|
80
|
+
- 🔎 [`npx vitest-auto-spy doctor`](#the-cli--doctor-perf-codemod-and-init) — suite-level defects **that never fail a run**: a `tsconfig` `include` matching no file, a production module importing a spec, a `@jest-environment` pragma the runner never reads, config left behind for a runner that is gone. Read-only, no config, exits 1 in CI
|
|
81
|
+
- ⏱️ [`npx vitest-auto-spy perf`](#perf--where-the-cpu-time-actually-goes) — where a suite's CPU time actually goes, phase by phase, and which spec files to act on: the ones that reach no DOM and could run under `node`, the ones that import a barrel. Runs Vitest once with a reporter this package ships, reads `TestModule.diagnostic()`, names files, states the rule behind each finding
|
|
79
82
|
- 🚚 [`npx vitest-auto-spy codemod`](#codemod--migrating-a-suite-off-jest-auto-spies) — thirteen transforms that move a suite off `jest-auto-spies` and Jest, or off `jasmine-auto-spies` and jasmine (`--from jasmine`), dry-run by default, with a `--verify` pass that also checks a file somebody edited by hand
|
|
80
83
|
- 🔇 Console spies — `import { consoleInfoSpy } from 'vitest-auto-spy/console'` silences `console` and asserts its calls
|
|
81
84
|
- 🧭 [**Spec patterns**](https://asdalexey.github.io/vitest-auto-spy/recipes) — the shapes a ~370-file Angular suite converged on, and the traps that only surface at scale
|
|
@@ -104,8 +107,9 @@ includes — that one import is what keeps `returnSubject()` typed as rxjs's own
|
|
|
104
107
|
- [Install](#install)
|
|
105
108
|
- [Requirements](#requirements)
|
|
106
109
|
- [Peer dependencies](#peer-dependencies)
|
|
107
|
-
- [The CLI — `doctor`, `codemod` and `init`](#the-cli--doctor-codemod-and-init)
|
|
110
|
+
- [The CLI — `doctor`, `perf`, `codemod` and `init`](#the-cli--doctor-perf-codemod-and-init)
|
|
108
111
|
- [`doctor` — defects that never fail](#doctor--defects-that-never-fail)
|
|
112
|
+
- [`perf` — where the CPU time actually goes](#perf--where-the-cpu-time-actually-goes)
|
|
109
113
|
- [`codemod` — migrating a suite off `jest-auto-spies`](#codemod--migrating-a-suite-off-jest-auto-spies)
|
|
110
114
|
- [`init` — the pointer an agent reads](#init--the-pointer-an-agent-reads)
|
|
111
115
|
- [Using this library with an AI agent](#using-this-library-with-an-ai-agent)
|
|
@@ -138,6 +142,8 @@ includes — that one import is what keeps `returnSubject()` typed as rxjs's own
|
|
|
138
142
|
- [Angular on Bun (`bun:test`)](#angular-on-bun-buntest)
|
|
139
143
|
- [Comparison](#comparison)
|
|
140
144
|
- [@testing-library/angular /vitest-utils](#testing-libraryangular-vitest-utils)
|
|
145
|
+
- [Benchmarks](#benchmarks)
|
|
146
|
+
- [Reproducing the numbers](#reproducing-the-numbers)
|
|
141
147
|
- [Migrating from jest-auto-spies](#migrating-from-jest-auto-spies)
|
|
142
148
|
- [Migrating from jasmine-auto-spies](#migrating-from-jasmine-auto-spies)
|
|
143
149
|
- [Migrating from @ngneat/spectator](https://asdalexey.github.io/vitest-auto-spy/migrating-spectator)
|
|
@@ -238,12 +244,13 @@ them only for the matching entry point. The package itself has **zero runtime de
|
|
|
238
244
|
| `rxjs` | `vitest-auto-spy/rxjs` observable spies — `>=7`, **no upper bound** (the rxjs 8 line included); not needed to type-check a spy since 4.0.0 | yes |
|
|
239
245
|
| `@angular/core` | `vitest-auto-spy/angular` helpers | yes |
|
|
240
246
|
|
|
241
|
-
## The CLI — `doctor`, `codemod` and `init`
|
|
247
|
+
## The CLI — `doctor`, `perf`, `codemod` and `init`
|
|
242
248
|
|
|
243
249
|
The package ships one executable, with no dependencies and nothing to configure:
|
|
244
250
|
|
|
245
251
|
```bash
|
|
246
252
|
npx vitest-auto-spy doctor # read-only. Exits 1 when it finds something
|
|
253
|
+
npx vitest-auto-spy perf # where the suite's CPU time goes. Always exits 0
|
|
247
254
|
npx vitest-auto-spy codemod # prints the migration diff. Writes nothing without --write
|
|
248
255
|
npx vitest-auto-spy init # writes the agent instructions pointer
|
|
249
256
|
```
|
|
@@ -291,6 +298,56 @@ spec tsconfigs still covered their specs.
|
|
|
291
298
|
|
|
292
299
|
`doctor` never writes. There is no `--fix`.
|
|
293
300
|
|
|
301
|
+
### `perf` — where the CPU time actually goes
|
|
302
|
+
|
|
303
|
+
Vitest prints one summary line per run with a phase breakdown; it is the only place those numbers
|
|
304
|
+
surface, and it is for the whole suite, not a file. `perf` reads the same numbers per file — through
|
|
305
|
+
`TestModule.diagnostic()`, Vitest's own public accessor, via a reporter this package ships — and
|
|
306
|
+
turns whichever phase dominates into a list of files and the rule that put them there. Nothing here
|
|
307
|
+
parses terminal output, and it always exits 0: a slow suite is not a failing one.
|
|
308
|
+
|
|
309
|
+
```bash
|
|
310
|
+
npx vitest-auto-spy perf # run the whole suite once and report
|
|
311
|
+
npx vitest-auto-spy perf src/cli # path passed through to Vitest as a file filter
|
|
312
|
+
npx vitest-auto-spy perf --json out/perf.json # re-analyse a report instead of running Vitest
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
```
|
|
316
|
+
$ npx vitest-auto-spy perf src/cli
|
|
317
|
+
vitest-auto-spy perf — /path/to/repo
|
|
318
|
+
16 test files, 860ms wall clock, 17.30s of CPU time summed over the workers
|
|
319
|
+
|
|
320
|
+
phase time share
|
|
321
|
+
prepare 6.34s 36.7%
|
|
322
|
+
environment 5.46s 31.6%
|
|
323
|
+
setup 3.01s 17.4%
|
|
324
|
+
transform 1.19s 6.9%
|
|
325
|
+
import 879ms 5.1%
|
|
326
|
+
tests 411ms 2.4%
|
|
327
|
+
|
|
328
|
+
info perf-environment
|
|
329
|
+
Environment setup is 31.6% of the measured CPU time, against 2.4% in the test bodies. No
|
|
330
|
+
spec file could be proved DOM-free, so this names none; 109 were left undecided.
|
|
331
|
+
→ Move what does not need a DOM to the `node` environment. …
|
|
332
|
+
|
|
333
|
+
0 errors, 0 warnings, 2 notes
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
That is this repository's own suite: `perf` names zero DOM-free candidates and leaves the rest
|
|
337
|
+
undecided, because `src/test-setup.ts` builds an Angular `TestBed` before every spec. The rule is
|
|
338
|
+
deliberately one-sided — a spec is a candidate only when it, the setup files, and every module they
|
|
339
|
+
import were read, none mentions a DOM name, and every package they import is on a short
|
|
340
|
+
DOM-free allowlist. Anything the rule cannot resolve is **undecided**, never assumed safe: a false
|
|
341
|
+
positive is somebody's suite failing on `document is not defined`.
|
|
342
|
+
|
|
343
|
+
**The phase totals are CPU time summed across workers**, which is why 860ms of wall clock reads as
|
|
344
|
+
17.30s of CPU above — the six phases are `environment`, `prepare`, `import`, `setup`, `tests` (all
|
|
345
|
+
per file) and `transform` (whole run). Where the isolation finding suggests `test.isolate: false`,
|
|
346
|
+
it links to this package's own memory measurements rather than repeating the numbers here — that
|
|
347
|
+
flag trades per-file cleanup for memory that grows with the suite.
|
|
348
|
+
|
|
349
|
+
Full reference, phase by phase and finding by finding: **[The CLI](https://asdalexey.github.io/vitest-auto-spy/utilities/cli)**.
|
|
350
|
+
|
|
294
351
|
### `codemod` — migrating a suite off `jest-auto-spies`
|
|
295
352
|
|
|
296
353
|
```bash
|
|
@@ -1129,6 +1186,175 @@ since 19.2.0 (2026-03-17), present in the tarball's `exports` map. That is a rea
|
|
|
1129
1186
|
favour. Full write-up, including ng-mocks and Spectator:
|
|
1130
1187
|
[Comparison → Angular](https://asdalexey.github.io/vitest-auto-spy/comparison#angular).
|
|
1131
1188
|
|
|
1189
|
+
## The spy engine
|
|
1190
|
+
|
|
1191
|
+
A method spy is not a `vi.fn()`. It is this library's own mock function — one shared prototype
|
|
1192
|
+
carrying the whole `Mock` surface, call state allocated on the first call rather than at creation,
|
|
1193
|
+
and no entry in any global registry. `vi.fn()` assigns some twenty-five closures as own properties
|
|
1194
|
+
of every mock it creates and allocates six arrays up front, per method, on every double a spec
|
|
1195
|
+
builds; on a service whose methods a test mostly never calls, that is the bill.
|
|
1196
|
+
|
|
1197
|
+
Everything a spec can observe is unchanged, and the suite pins it by putting a spy and a `vi.fn()`
|
|
1198
|
+
through the same steps and comparing their state:
|
|
1199
|
+
|
|
1200
|
+
- `vi.isMockFunction`, every `expect` matcher (`toHaveBeenCalledWith`, `toHaveReturned`,
|
|
1201
|
+
`toHaveResolved`, `toHaveBeenNthCalledWith`, …), and the failure messages that name the mock.
|
|
1202
|
+
- `spy.method.mock.calls` / `.results` / `.settledResults` / `.instances` / `.contexts` /
|
|
1203
|
+
`.lastCall`, the whole `mockReturnValue` / `mockResolvedValue` / `mockImplementation` family,
|
|
1204
|
+
`withImplementation`, `mockClear` / `mockReset` / `mockRestore`, `mockName`, `using`.
|
|
1205
|
+
- `vi.clearAllMocks()`, `vi.resetAllMocks()`, and the `clearMocks` / `mockReset` config keys, which
|
|
1206
|
+
Vitest applies through those two.
|
|
1207
|
+
|
|
1208
|
+
It is measured in bytes as well as microseconds. A materialised method went from **5 445 B to
|
|
1209
|
+
1 929 B** — the hand-written control, which is the runner's own mock with no library in the way,
|
|
1210
|
+
retains 5 169 B — and an eagerly built double that nothing calls went from 4 418 to **632 B per
|
|
1211
|
+
method**, because a spy that is never called now allocates none of the six arrays `vi.fn()`
|
|
1212
|
+
allocates up front. Full tables:
|
|
1213
|
+
[Performance → retained memory per double](https://asdalexey.github.io/vitest-auto-spy/core/performance#retained-memory-per-double).
|
|
1214
|
+
|
|
1215
|
+
**The one difference.** `mock.invocationCallOrder` counts on this library's own scale, so
|
|
1216
|
+
`expect(a).toHaveBeenCalledBefore(b)` is exact between two auto-spies and meaningless between an
|
|
1217
|
+
auto-spy and a hand-written `vi.fn()`. If a suite needs that comparison, switch the run back:
|
|
1218
|
+
|
|
1219
|
+
```ts
|
|
1220
|
+
// vitest.setup.ts
|
|
1221
|
+
import { setSpyEngine } from 'vitest-auto-spy/setup';
|
|
1222
|
+
|
|
1223
|
+
setSpyEngine('runner'); // every double built afterwards uses vi.fn() per method
|
|
1224
|
+
```
|
|
1225
|
+
|
|
1226
|
+
The switch is Vitest-only. On Bun and `node:test` the runner's own matchers recognise only the
|
|
1227
|
+
runner's own mocks, so those entries keep building spies from `mock()` and `t.mock.fn()`.
|
|
1228
|
+
|
|
1229
|
+
## Benchmarks
|
|
1230
|
+
|
|
1231
|
+
Wall-clock at suite scale varies **15-20% between invocations** of the identical configuration —
|
|
1232
|
+
the same cell measured 1.56 s and 1.33 s at 1 000 tests, 10.94 s and 12.51 s at 10 000. Peak RSS, by
|
|
1233
|
+
contrast, reproduces tightly and separates arms by multiples. Treat the ratios below as the durable
|
|
1234
|
+
figure and any single wall-clock number as describing one machine, not a promise about yours.
|
|
1235
|
+
|
|
1236
|
+
**The micro-benchmark below has the same problem, measured directly.** Every micro figure is the
|
|
1237
|
+
**median p75 of seven independent runs**, each its own process, at doubled iteration budgets, run
|
|
1238
|
+
with `npm run bench:vs:precise` — not a single run. What each row's ± column reports is how far that
|
|
1239
|
+
published median can be off: across the 47 rows of the canonical run, a median of **±1.1%** and at
|
|
1240
|
+
worst **±3.8%**. The blunter rule still holds for reading a single local run — **a difference under
|
|
1241
|
+
about 20% is not worth quoting** — and the narrowest margin below is 2.19×.
|
|
1242
|
+
|
|
1243
|
+
`jest-auto-spies@3.0.1`, `jasmine-auto-spies@8.0.1` and `@bugsplat/vitest-auto-spies@1.0.0` are all
|
|
1244
|
+
measured directly here — no more standing in for one another. All three depend on
|
|
1245
|
+
`@hirez_io/auto-spies-core@3.0.0` and differ only in the spy factory they hand it (`jest.fn()`,
|
|
1246
|
+
`jasmine.createSpy()`, `vi.fn()`), and on the micro-benchmark they land within a few per cent of each
|
|
1247
|
+
other on every case — see the 40-methods/3-called row in
|
|
1248
|
+
[Performance](https://asdalexey.github.io/vitest-auto-spy/core/performance#micro-benchmark).
|
|
1249
|
+
`jest-auto-spies` and `jasmine-auto-spies` run here under a minimal `jest` / `jasmine` global backed
|
|
1250
|
+
by `vi.fn()`, so every arm creates the same underlying mock and the runner's own per-mock cost is a
|
|
1251
|
+
shared constant — what these numbers describe is each library's own code, not what a real Jest or
|
|
1252
|
+
Jasmine suite would show.
|
|
1253
|
+
|
|
1254
|
+
The micro-benchmark runs every arm inside a block to the same iteration count (`n`), not the same
|
|
1255
|
+
time budget — these cases allocate doubles by the tens of thousands, and GC cost scales with objects
|
|
1256
|
+
created rather than elapsed time, so a fixed time budget would have handed a faster arm more
|
|
1257
|
+
allocations and made it pay for its own speed.
|
|
1258
|
+
|
|
1259
|
+
**Every micro-benchmark table is this library's**, and the margin is against the *best* other arm in
|
|
1260
|
+
that table. The three class sizes are measured project profiles, not round numbers — across four
|
|
1261
|
+
private Angular suites (~2 700 spec files, 2 742 doubles built from a class) the median service has
|
|
1262
|
+
5-8 methods and the spec touches 1 of them:
|
|
1263
|
+
|
|
1264
|
+
| Case (micro) | 4.0 | 4.1 | best other arm | lead |
|
|
1265
|
+
| --- | ---: | ---: | ---: | ---: |
|
|
1266
|
+
| small project — 6 methods, 1 called | — | **1.42 µs** | 6.83 µs hand-written | 4.82x |
|
|
1267
|
+
| medium project — 14 methods, 2 called | — | **2.67 µs** | 15.92 µs hand-written | 5.97x |
|
|
1268
|
+
| large project — 45 methods, 2 called | — | **5.79 µs** | 52.79 µs hand-written | 9.11x |
|
|
1269
|
+
| worst case — all 14 of 14 methods called | 18.92 µs (0.66x, a loss) | **8.17 µs** | 17.92 µs hand-written | 2.19x |
|
|
1270
|
+
| worst case — all 45 of 45 methods called | 75.33 µs (0.71x, a loss) | **26.12 µs** | 62.04 µs hand-written | 2.37x |
|
|
1271
|
+
| double from a type — 2 / 10 / 40 members | 3.58 / 18.17 / 72.88 µs (a loss) | **1.00 / 4.67 / 18.92 µs** | 2.79 / 13.83 / 56.79 µs vitest-mock-extended | 2.79-3.00x |
|
|
1272
|
+
| deep double, 3 levels, leaf called | 8.83 µs (0.61x, a loss) | **2.29 µs** | 5.46 µs vitest-mock-extended | 2.38x |
|
|
1273
|
+
| configure a return + 3 calls — class / type | 3.29 / 2.08 µs (type a loss) | **2.21 / 0.71 µs** | 16.38 µs hand-written / 1.58 µs @golevelup | 7.41x / 2.24x |
|
|
1274
|
+
| `calledWith` dispatch, 2 configured + 1 miss | 0.54 µs (parity) | **0.17 µs** | 0.54 µs vitest-mock-extended | 3.25x |
|
|
1275
|
+
|
|
1276
|
+
Six of those rows were losses and one was parity. What changed is that a method spy is no longer a
|
|
1277
|
+
`vi.fn()` — see [The spy engine](#the-spy-engine) — plus one thing that is not the engine: a
|
|
1278
|
+
`calledWith(x)` of a single primitive argument used to be rendered into a string key on *every call*
|
|
1279
|
+
of that spy, where a `Map` keyed by the value does the same lookup with no allocation.
|
|
1280
|
+
|
|
1281
|
+
The three class sizes changed in the same release, from round numbers to measured profiles, so the
|
|
1282
|
+
first two columns are the same operation on a slightly different class where the row says so. Part
|
|
1283
|
+
of the lead is that this library no longer pays the runner's per-mock cost while every other arm
|
|
1284
|
+
still does — a difference in the product rather than in the measurement, and the `hand-written
|
|
1285
|
+
vi.fn() per method` arm is in the table precisely so its size stays visible.
|
|
1286
|
+
|
|
1287
|
+
**Where it still loses, same weight, same table** (`npm run bench:suite`, re-measured 2026-09-03 on
|
|
1288
|
+
the 4.1 build, 20-method class, `isolate: true`, coverage on, two runs of 3 rounds each, medians of
|
|
1289
|
+
all six):
|
|
1290
|
+
|
|
1291
|
+
| Comparison | 1 000 tests | 3 000 tests | 10 000 tests |
|
|
1292
|
+
| --- | ---: | ---: | ---: |
|
|
1293
|
+
| vitest-auto-spy | 1.33 s | 3.19 s | 10.66 s |
|
|
1294
|
+
| hand-written `vi.fn()` | 1.33 s (0.99×) | 3.08 s (0.97×) | 10.07 s (0.94×) |
|
|
1295
|
+
| @bugsplat/vitest-auto-spies | 2.21 s (1.66×) | 5.50 s (1.72×) | 17.40 s (1.63×) |
|
|
1296
|
+
|
|
1297
|
+
Hand-written doubles are still cheaper — **about 5% at the median**, down from 10-15% before 4.1,
|
|
1298
|
+
with the 18 per-round ratios spread 0.81-1.01 — and that row is the proof that micro-benchmark
|
|
1299
|
+
multipliers do not transfer to suite scale: building a double is on the order of 1% of a test's cost,
|
|
1300
|
+
so a 10× win on the double shows up as a few per cent on the run, and nearly everything else in a
|
|
1301
|
+
suite swamps it.
|
|
1302
|
+
|
|
1303
|
+
**Memory, `isolate: false`, 100-method class, 10 000 tests, peak RSS:**
|
|
1304
|
+
|
|
1305
|
+
| Arm | Peak RSS |
|
|
1306
|
+
| --- | ---: |
|
|
1307
|
+
| hand-written `vi.fn()` | 6733 MB |
|
|
1308
|
+
| vitest-auto-spy, default | 2475 MB |
|
|
1309
|
+
| vitest-auto-spy, `lazySpies: 'proxy'` | 2109 MB |
|
|
1310
|
+
|
|
1311
|
+
Hand-written doubles use 2.7x the default's peak RSS here — the difference between finishing and an
|
|
1312
|
+
OOM'd worker on a large suite.
|
|
1313
|
+
|
|
1314
|
+
That gap traces down to a single double: on the same 100-method class, untouched, `lazySpies:
|
|
1315
|
+
'proxy'` retains **4 097 B** per double against a hand-written double's **414 462 B** — the
|
|
1316
|
+
per-double number that gets multiplied by every double a file holds alive, and the setting that
|
|
1317
|
+
decides whether a large suite survives `isolate: false`. Full retained-memory tables:
|
|
1318
|
+
[Performance](https://asdalexey.github.io/vitest-auto-spy/core/performance#retained-memory-per-double).
|
|
1319
|
+
|
|
1320
|
+
**Guidance keyed to suite size:** `isolate: false` is worth about 3x on its own — more than any
|
|
1321
|
+
library choice measured here — and it makes memory, not wall-clock, the binding constraint. On a
|
|
1322
|
+
wide class under `isolate: false`, add `lazySpies: 'proxy'` for about 15% less peak RSS. Do not turn
|
|
1323
|
+
`'proxy'` on under normal isolation (`isolate: true`): there it measurably does nothing (5/7 rounds
|
|
1324
|
+
either side of parity).
|
|
1325
|
+
|
|
1326
|
+
Full write-up, methodology and the complete suite-scale tables:
|
|
1327
|
+
[Performance](https://asdalexey.github.io/vitest-auto-spy/core/performance).
|
|
1328
|
+
|
|
1329
|
+
### Reproducing the numbers
|
|
1330
|
+
|
|
1331
|
+
Node **>=18** (the project floor); the numbers above were captured on Node v24.19.0. The steps are
|
|
1332
|
+
identical on Windows, macOS and Linux — same Node, same npm, same commands, nothing OS-specific to
|
|
1333
|
+
call out:
|
|
1334
|
+
|
|
1335
|
+
```bash
|
|
1336
|
+
git clone https://github.com/ASDAlexey/vitest-auto-spy.git
|
|
1337
|
+
cd vitest-auto-spy
|
|
1338
|
+
npm ci
|
|
1339
|
+
npm ci --prefix bench
|
|
1340
|
+
npm run bench:vs
|
|
1341
|
+
```
|
|
1342
|
+
|
|
1343
|
+
Two installs are intentional: the competitor packages live in `bench/package.json`, kept out of the
|
|
1344
|
+
root so it carries nothing it doesn't ship or test with, and `bench/.npmrc` pins
|
|
1345
|
+
`legacy-peer-deps=true` so npm doesn't install a second copy of `vitest` beside the root one — two
|
|
1346
|
+
copies would mean two mock registries and the run dies out of memory.
|
|
1347
|
+
|
|
1348
|
+
`npm run bench:vs` is the micro-benchmark, a single run in about 55 seconds — the right form for
|
|
1349
|
+
local iteration. `npm run bench:vs -- --repeat 5` is what produced the median p75 figures published
|
|
1350
|
+
above, about 4.5 minutes for five independent runs in five processes. For the suite-scale tables, see
|
|
1351
|
+
`npm run bench:suite --help` — it takes **tens of minutes at 10 000 tests**, so budget for that
|
|
1352
|
+
before running it locally.
|
|
1353
|
+
|
|
1354
|
+
The same benchmark also runs in CI — on a monthly schedule, on any pull request touching `bench/**`
|
|
1355
|
+
or `src/lib/**`, and on demand — so a run you didn't trigger yourself is checkable in
|
|
1356
|
+
[Actions → Benchmarks](https://github.com/ASDAlexey/vitest-auto-spy/actions/workflows/bench.yml).
|
|
1357
|
+
|
|
1132
1358
|
## Migrating from jest-auto-spies
|
|
1133
1359
|
|
|
1134
1360
|
The public API is intentionally identical. In most projects the migration is a
|
|
@@ -2211,6 +2437,7 @@ single-purpose utility you can pick up independently — they all ride on the sa
|
|
|
2211
2437
|
| `restoreTimerGlobals()` | `/setup` | Put back timer globals that uninstalling the fakes deleted rather than restored |
|
|
2212
2438
|
| `trackMockRegistry()` / `keepMockRegistered(mock)` / `restoreLongLivedImplementations()` | `/setup` | Keep @vitest/spy's mock registry to the mocks that outlive a file; mark one the split would miss; put back an implementation a cross-file `vi.resetAllMocks()` dropped ([details](#test-run-hygiene)) |
|
|
2213
2439
|
| `trackNodeMocks()` / `pruneNodeMocks()` / `countNodeMocks()` | `/node` | Give this library its own `node:test` `MockTracker` so a dropped spy is freed — 21× less retained heap; sweep by hand, and read the count back |
|
|
2440
|
+
| `setSpyEngine(engine)` / `getSpyEngine()` | `/setup` | Build method spies from this library's own mock (`'auto-spy'`, the default) or from `vi.fn()` (`'runner'`) ([details](#the-spy-engine)) |
|
|
2214
2441
|
| `errorHandler` | core | The `mustBeCalledWith` argument-mismatch reporter — swap it to customize failure output |
|
|
2215
2442
|
|
|
2216
2443
|
A taste of the DI pair — provide the spy, inject it back fully typed:
|
|
@@ -2871,7 +3098,7 @@ as `methodsToSpyOn`, named for callables that live on the instance — `signal()
|
|
|
2871
3098
|
`gettersToSpyOn`, `settersToSpyOn`, `autoSpyAccessors` (discover every getter/setter),
|
|
2872
3099
|
`fillMissing` (answer a name the prototype never carried with a spy — for a **partially** abstract
|
|
2873
3100
|
class, where `abstract` members are erased and the empty-prototype fallback no longer fires),
|
|
2874
|
-
`lazySpies` (materialize method spies on first access — cheaper for wide classes; the `provideAutoSpy` default on Angular. `'proxy'` keeps the laziness and drops the per-method placeholder
|
|
3101
|
+
`lazySpies` (materialize method spies on first access — cheaper for wide classes; the `provideAutoSpy` default on Angular. `'proxy'` keeps the laziness and drops the per-method placeholder — no memory crossover at any width measured, lighter than the default from 10 methods up (4 097 B vs the default's 25 601 B per double retained, untouched, on a 100-method class); it costs +30 ns per read once a method has materialised, which only pays back in **time** on classes above ~20 methods — see [Performance](https://asdalexey.github.io/vitest-auto-spy/core/performance#retained-memory-per-double))
|
|
2875
3102
|
|
|
2876
3103
|
`ValueConfig` (for `nextWithValues`): `{ value, delay? }` | `{ errorValue, delay? }` | `{ complete?, delay? }`.
|
|
2877
3104
|
|