{"id":51377203,"url":"https://github.com/ASDAlexey/vitest-auto-spy","last_synced_at":"2026-07-04T19:00:30.300Z","repository":{"id":366420332,"uuid":"1276081798","full_name":"ASDAlexey/vitest-auto-spy","owner":"ASDAlexey","description":"Auto-spy test doubles from a class for Vitest + Angular — a jest-auto-spies drop-in replacement.","archived":false,"fork":false,"pushed_at":"2026-06-29T05:35:25.000Z","size":888,"stargazers_count":4,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-06-29T07:16:49.790Z","etag":null,"topics":["angular","auto-spies","jest-auto-spies","mocking","rxjs","spy","testing","typescript","unit-testing","vitest","zoneless"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/vitest-auto-spy","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ASDAlexey.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-21T14:18:04.000Z","updated_at":"2026-06-29T05:35:30.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ASDAlexey/vitest-auto-spy","commit_stats":null,"previous_names":["asdalexey/vitest-auto-spy"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/ASDAlexey/vitest-auto-spy","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ASDAlexey%2Fvitest-auto-spy","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ASDAlexey%2Fvitest-auto-spy/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ASDAlexey%2Fvitest-auto-spy/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ASDAlexey%2Fvitest-auto-spy/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ASDAlexey","download_url":"https://codeload.github.com/ASDAlexey/vitest-auto-spy/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ASDAlexey%2Fvitest-auto-spy/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35132289,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-04T02:00:05.987Z","response_time":113,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["angular","auto-spies","jest-auto-spies","mocking","rxjs","spy","testing","typescript","unit-testing","vitest","zoneless"],"created_at":"2026-07-03T14:00:27.942Z","updated_at":"2026-07-04T19:00:30.242Z","avatar_url":"https://github.com/ASDAlexey.png","language":"TypeScript","funding_links":[],"categories":["Testing","Packages"],"sub_categories":["Helpers","Mocking"],"readme":"\u003cdiv align=\"center\"\u003e\n\n# vitest-auto-spy\n\n**Auto-generate fully-typed test spies from a class — across any Vitest-compatible runtime and framework.**\n\nThe only auto-spy library that reads a **class** and gives a **fully-typed** spy of every method\nwith **return-type-aware** helpers (`resolveWith` / `nextWith` / `calledWith`). Runs on **Vitest**,\n**Bun** (`bun:test`) and **`node:test`** behind one identical API, with **RxJS** spies and\n**Angular / NestJS / React / Vue·Pinia / Svelte** recipes ([availability](#availability)). A drop-in\nreplacement for [`jest-auto-spies`](https://www.npmjs.com/package/jest-auto-spies) — same API.\n\n[![npm version](https://img.shields.io/npm/v/vitest-auto-spy?color=cb3837\u0026logo=npm)](https://www.npmjs.com/package/vitest-auto-spy)\n[![npm downloads](https://img.shields.io/npm/dm/vitest-auto-spy?color=cb3837\u0026logo=npm)](https://www.npmjs.com/package/vitest-auto-spy)\n[![CI](https://github.com/ASDAlexey/vitest-auto-spy/actions/workflows/ci.yml/badge.svg)](https://github.com/ASDAlexey/vitest-auto-spy/actions/workflows/ci.yml)\n[![minzipped size](https://img.shields.io/bundlephobia/minzip/vitest-auto-spy?label=minzip)](https://bundlephobia.com/package/vitest-auto-spy)\n[![types](https://img.shields.io/npm/types/vitest-auto-spy?logo=typescript\u0026logoColor=white)](https://www.npmjs.com/package/vitest-auto-spy)\n[![coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/ASDAlexey/vitest-auto-spy/actions/workflows/ci.yml)\n[![license](https://img.shields.io/npm/l/vitest-auto-spy?color=blue)](./LICENSE)\n\n[![Vitest](https://img.shields.io/badge/Vitest-✓-6E9F18?logo=vitest\u0026logoColor=white)](#runtimes)\n[![Bun](https://img.shields.io/badge/Bun-✓-6E9F18?logo=bun\u0026logoColor=white)](#availability)\n[![node:test](https://img.shields.io/badge/node%3Atest-✓-6E9F18?logo=node.js\u0026logoColor=white)](#availability)\n[![runtime deps](https://img.shields.io/badge/runtime%20deps-0-brightgreen)](#install)\n\n📦 [**npm**](https://www.npmjs.com/package/vitest-auto-spy) · 🐙 [**GitHub**](https://github.com/ASDAlexey/vitest-auto-spy) · 🔖 [**Changelog**](./CHANGELOG.md)\n\n\u003c/div\u003e\n\n---\n\n- 🧪 Reads a class and generates a typed spy for **every** method — no hand-written `vi.fn()` lists\n- 🧬 Or mock from a **type/interface** alone — `createAutoMock\u003cT\u003e()`, no class required\n- 🌐 One `MockAdapter` core — **Vitest**, **Bun** and **`node:test`**, identical API on each\n- 🧩 Framework recipes: **Angular**, **NestJS**, **React**, **Vue/Pinia** and **Svelte**\n- 🎯 Return-type-aware helpers — sync, `Promise`, and `Observable` all get the right API\n- 🔀 `calledWith` / `mustBeCalledWith` argument dispatch\n- 📡 First-class RxJS `Observable` spying (`nextWith`, `nextWithValues`, `throwWith`, …)\n- ⚙️ Getter / setter spies via `accessorSpies`\n- 🧰 DI \u0026 mocking utilities — `provideAutoSpy` / `injectSpy` (Angular, NestJS, Vue), `createFunctionSpy`, `mockReadonlyProp` for signals\n- 🔇 Console spies — `import { consoleInfoSpy } from 'vitest-auto-spy/console'` silences `console` and asserts its calls\n- 🟢 100% test coverage, **zero runtime dependencies** (in-tree arg serializer, no `javascript-stringify`)\n\n## Table of contents\n\n- [Install](#install)\n- [Availability](#availability)\n- [Quick start](#quick-start)\n- [Why](#why)\n- [How it works (and what it won't spy)](#how-it-works-and-what-it-wont-spy)\n- [Comparison](#comparison)\n- [Migrating from jest-auto-spies](#migrating-from-jest-auto-spies)\n- [Configuration](#configuration)\n- [Auto-mock by type (no class needed)](#auto-mock-by-type-no-class-needed)\n- [Synchronous methods](#synchronous-methods)\n- [Promise-returning methods](#promise-returning-methods)\n- [Observable methods \u0026 properties](#observable-returning-methods--observable-properties)\n- [Getters \u0026 setters](#getters--setters)\n- [Framework adapters](#framework-adapters)\n  - [NestJS](#nestjs)\n  - [React (Testing Library)](#react-testing-library)\n  - [Vue / Pinia](#vue--pinia)\n  - [Svelte](#svelte)\n  - [Angular](#angular)\n- [Utilities](#utilities)\n- [API reference](#api-reference)\n- [FAQ \u0026 troubleshooting](#faq--troubleshooting)\n- [Versioning](#versioning)\n- [Contributing](#contributing)\n- [Acknowledgements](#acknowledgements)\n- [License](#license)\n\n## Install\n\n```bash\nnpm i -D vitest-auto-spy\n```\n\n### Requirements\n\n| Tool | Minimum |\n| --- | --- |\n| Node.js | ≥ 18 |\n| Vitest | ≥ 1.0 (required peer) |\n| TypeScript | ≥ 4.7 for the typed helpers (plain JS works too, just untyped) |\n\nShips **dual ESM + CommonJS** with bundled `.d.ts` types, so it drops into both `import`- and\n`require`-style test setups.\n\n### Peer dependencies\n\nAll peers are **provided by your project**; `rxjs` and `@angular/core` are **optional** — install\nthem only for the matching entry point. The package itself has **zero runtime dependencies**.\n\n| Peer | Needed for | Optional? |\n| --- | --- | --- |\n| `vitest` | the default runner | no |\n| `rxjs` | `vitest-auto-spy/rxjs` observable spies (and `Spy\u003cT\u003e` type-checking) | yes |\n| `@angular/core` | `vitest-auto-spy/angular` helpers | yes |\n\n## Availability\n\n\u003e **All entry points are published.** The **Vitest / Bun / `node:test`** runtimes, the **RxJS** layer,\n\u003e and the **Angular / NestJS / React / Vue·Pinia / Svelte** recipes all ship as importable entry points —\n\u003e one identical API across every runner and framework.\n\n| Entry point | Status |\n| --- | --- |\n| `vitest-auto-spy` · `vitest-auto-spy/rxjs` · `vitest-auto-spy/angular` | ✅ **Published** |\n| `vitest-auto-spy/bun` · `vitest-auto-spy/node` | ✅ **Published** |\n| `vitest-auto-spy/nestjs` · `/react` · `/vue` · `/svelte` · `/console` | ✅ **Published** |\n\n## Quick start\n\nPass a class — every method becomes a typed spy, and the **constructor is never called** (no side\neffects). The helper you get on each method matches its return type:\n\n```ts\nimport { beforeEach, expect, it } from 'vitest';\nimport { createSpyFromClass, type Spy } from 'vitest-auto-spy';\n\nclass UserService {\n  getName(id: number): string {\n    return 'real name';\n  }\n  async getUser(id: number): Promise\u003c{ id: number; name: string }\u003e {\n    return fetchUser(id);\n  }\n}\n\nlet userService: Spy\u003cUserService\u003e;\n\nbeforeEach(() =\u003e {\n  userService = createSpyFromClass(UserService); // every method is now a spy\n});\n\nit('stubs each method with the right helper for its return type', async () =\u003e {\n  userService.getName.mockReturnValue('Ada'); // sync\n  userService.getUser.resolveWith({ id: 1, name: 'Ada' }); // Promise helper\n\n  expect(userService.getName(1)).toBe('Ada');\n  await expect(userService.getUser(1)).resolves.toEqual({ id: 1, name: 'Ada' });\n  expect(userService.getName).toHaveBeenCalledWith(1);\n});\n```\n\nNo class, only a TypeScript type? Reach for\n[`createAutoMock\u003cT\u003e()`](#auto-mock-by-type-no-class-needed).\n\n## Why\n\nManually mocking a service is tedious and brittle:\n\n```ts\n// 😫  the old way\nconst userService = {\n  getUser: vi.fn(),\n  getUserList: vi.fn(),\n  // ...one line per method, kept in sync by hand\n};\n```\n\n`createSpyFromClass` reads the class and generates a typed spy for **every** method:\n\n```ts\n// 😎  the auto-spy way\nlet userService: Spy\u003cUserService\u003e;\n\nbeforeEach(() =\u003e {\n  userService = createSpyFromClass(UserService);\n});\n```\n\n`Spy\u003cUserService\u003e` exposes each method as a `vi.fn()` **plus** the right helpers based on\nthe method's return type (sync / `Promise` / `Observable`).\n\n## How it works (and what it won't spy)\n\n`createSpyFromClass(MyService)` reads `MyService.prototype` and walks the **prototype chain** — it\nnever `new`s the class. Concretely:\n\n- ✅ **The class is never instantiated.** The constructor and its side effects (HTTP clients, DB\n  connections, `inject()` calls) never run — you pass the class itself, not an instance.\n- ✅ **Inherited methods are spied too**, all the way up the prototype chain.\n- ✅ Each method is replaced by a fresh spy carrying the helpers that match its **return type**:\n  sync → `mockReturnValue` / `calledWith`; `Promise` → `resolveWith` / `rejectWith`; `Observable`\n  → `nextWith` / `throwWith` / … .\n\nWhat it **won't** auto-discover — by design, because these aren't prototype methods:\n\n- ⚠️ **Arrow-function class fields** (`doThing = () =\u003e {}`) are instance properties set in the\n  constructor, so prototype scanning can't see them. Use regular methods, list them explicitly, or\n  mock them by hand. (Same constraint as `jest-auto-spies`.)\n- ⚠️ **Getters / setters** are skipped unless named in `gettersToSpyOn` / `settersToSpyOn` — see\n  [Getters \u0026 setters](#getters--setters).\n- ⚠️ **Plain data properties** carry no value until you set one; auto-spy mocks *behaviour*\n  (methods), not state. To mock by type including properties, use\n  [`createAutoMock`](#auto-mock-by-type-no-class-needed).\n\n## Entry points \u0026 runtimes\n\nThe library ships a framework-agnostic core plus runtime and framework layers, so a plain\nNode / Bun / React / Vue project pulls **neither rxjs nor Angular into its runtime bundle**:\n\n| Import | Provides | Pulls in | Status |\n| --- | --- | --- | :---: |\n| `vitest-auto-spy` | `createSpyFromClass`, `createAutoMock`, `createFunctionSpy`, sync + promise + accessor spies, `errorHandler`, types | `vitest` | ✅ |\n| `vitest-auto-spy/rxjs` | observable spies (`nextWith`, `nextWithValues`, `observablePropsToSpyOn`, …) + `createObservableWithValues` | `rxjs` | ✅ |\n| `vitest-auto-spy/angular` | `provideAutoSpy`, `injectSpy`, `mockReadonlyProp*`, `mockAccessorsProp` | `@angular/core` | ✅ |\n| `vitest-auto-spy/bun` | the same core, driven by Bun's `bun:test` mocks | `bun:test` | ✅ |\n| `vitest-auto-spy/node` | the same core, driven by `node:test`'s `mock.fn()` | `node:test` | ✅ |\n| `vitest-auto-spy/nestjs` | `provideAutoSpy`, `injectSpy` for `Test.createTestingModule` | — (your `@nestjs/*`) | ✅ |\n| `vitest-auto-spy/react` | the core, with a natural import for React Testing Library suites | — (your `react`) | ✅ |\n| `vitest-auto-spy/vue` | `provideAutoSpy` for `global.provide` + Pinia store spying | — (your `vue`/`pinia`) | ✅ |\n| `vitest-auto-spy/svelte` | the core, with a natural import for Svelte suites | — (your `svelte`) | ✅ |\n| `vitest-auto-spy/console` | `consoleInfoSpy` \u0026 friends — silent typed spies over the global `console`, installed on import | `vitest` | ✅ |\n\n✅ all entry points published (see [Availability](#availability)).\n\n\u003e The framework subpaths import **nothing** from their framework — the helpers are structural, so\n\u003e `@nestjs/*`, `react`, `vue`/`pinia` and `svelte` stay your own (already-present) dev dependencies and\n\u003e never reach this package's runtime bundle.\n\n```ts\nimport { createSpyFromClass } from 'vitest-auto-spy';\nimport 'vitest-auto-spy/rxjs'; // once (e.g. in your test setup) — enables observable spies\nimport { provideAutoSpy, injectSpy } from 'vitest-auto-spy/angular';\n```\n\n### Runtimes\n\nThe core is runner-agnostic behind a `MockAdapter`: pick the entry that matches your test\nrunner — the public API (`createSpyFromClass`, `calledWith`, `resolveWith`, `nextWith`, …) is\nidentical across all three.\n\n```ts\nimport { createSpyFromClass } from 'vitest-auto-spy'; // Vitest (default, zero-config)\nimport { createSpyFromClass } from 'vitest-auto-spy/bun'; // Bun — bun:test\nimport { createSpyFromClass } from 'vitest-auto-spy/node'; // node:test\n```\n\n\u003e Only the auto-spy helpers are normalised across runtimes; **native** mock methods stay the\n\u003e runner's own — `mockReturnValue` on Vitest/Bun, `spy.method.mock.mockImplementation` on\n\u003e `node:test`. Each entry registers its adapter on import, so import the one matching your runner.\n\n\u003e Using an observable spy (`observablePropsToSpyOn`, `nextWith`, …) without importing\n\u003e `vitest-auto-spy/rxjs` throws a clear hint telling you to add that import.\n\u003e\n\u003e The decoupling is at the **runtime** level. The core's _type_ surface (`Spy\u003cT\u003e`) still\n\u003e references rxjs types, so keep `rxjs` available for type-checking (it's normally already a\n\u003e devDependency); none of it reaches your runtime bundle.\n\u003e\n\u003e The same inversion-of-control applies to the **test runner**: the core no longer imports\n\u003e `vitest` directly — `vi.fn()` / `vi.spyOn()` sit behind a `MockAdapter` that the\n\u003e `vitest-auto-spy` entry registers by default, so it stays zero-config. This is the groundwork\n\u003e for running the exact same core on other Vitest-compatible runners.\n\n## Comparison\n\n| Library | Reads a class? | Return-type-aware helpers? | Runtimes | We win on |\n| --- | :---: | :---: | --- | --- |\n| **vitest-auto-spy** | ✅ | ✅ | Vitest (Bun · node:test next) | — |\n| [jest-auto-spies](https://www.npmjs.com/package/jest-auto-spies) | ✅ | ✅ | Jest only | Vitest/Bun/Node successor, **same API** — direct migration path |\n| [vitest-mock-extended](https://www.npmjs.com/package/vitest-mock-extended) | ❌ (Proxy) | ❌ | Vitest | Return-type ergonomics **and** reading a real class (we also ship a Proxy mode: [`createAutoMock`](#auto-mock-by-type-no-class-needed)) |\n| [@golevelup/ts-vitest](https://www.npmjs.com/package/@golevelup/ts-vitest) | partial | ❌ | Vitest | Typed `Promise`/`Observable` helpers + explicit class→spy + `mustBeCalledWith` |\n| [sinon](https://www.npmjs.com/package/sinon) | ❌ (manual) | ❌ | Any | Auto-generated + fully typed vs. manual + loosely typed |\n\n**The pitch:** the only auto-spy library that reads a **class** and gives a **fully-typed** spy of\nevery method with **return-type-aware** control helpers (`resolveWith` / `nextWith` / `calledWith`) —\nacross any Vitest-compatible runtime and framework.\n\n## Migrating from jest-auto-spies\n\nThe public API is intentionally identical. In most projects the migration is a\n**find-and-replace of the import**:\n\n```diff\n- import { createSpyFromClass, provideAutoSpy } from 'jest-auto-spies';\n+ import { createSpyFromClass } from 'vitest-auto-spy';\n+ import { provideAutoSpy } from 'vitest-auto-spy/angular';\n+ import 'vitest-auto-spy/rxjs'; // once, if you use observable spies\n```\n\nThe only API-shape change from `jest-auto-spies` is that the Angular helpers and the\nobservable layer live behind the `/angular` and `/rxjs` subpaths (see [Entry points \u0026 runtimes](#entry-points--runtimes)).\n\n| jest-auto-spies | vitest-auto-spy | Status |\n| --- | --- | --- |\n| `createSpyFromClass` | `createSpyFromClass` | ✅ identical |\n| `provideAutoSpy` | `provideAutoSpy` | ✅ identical |\n| `calledWith` / `mustBeCalledWith` | same | ✅ identical |\n| `resolveWith` / `rejectWith` / `resolveWithPerCall` | same | ✅ identical |\n| `nextWith` / `nextOneTimeWith` / `nextWithValues` / `nextWithPerCall` | same | ✅ identical |\n| `throwWith` / `complete` / `returnSubject` | same | ✅ identical |\n| `accessorSpies.getters/setters` | same | ✅ identical |\n| `createObservableWithValues` | same | ✅ identical |\n| underlying mock | `jest.fn()` → `vi.fn()` | 🔁 swapped |\n\nJust make sure your tests run under Vitest, and (for Angular) that `TestBed` is set up.\n\n## Configuration\n\n```ts\n// 1. all methods (default)\ncreateSpyFromClass(MyService);\n\n// 2. only these methods\ncreateSpyFromClass(MyService, ['getName', 'getAge']);\n\n// 3. full config object\ncreateSpyFromClass(MyService, {\n  methodsToSpyOn: ['getName'],\n  observablePropsToSpyOn: ['products$'], // Observable *properties*\n  gettersToSpyOn: ['userName'],\n  settersToSpyOn: ['userName'],\n});\n```\n\n## Auto-mock by type (no class needed)\n\n`createSpyFromClass` reads a real class's prototype. When you only have a TypeScript **interface or\ntype** (no runtime class), use `createAutoMock\u003cT\u003e()` — it builds the spy lazily from the type alone,\nvia a `Proxy`:\n\n```ts\nimport { createAutoMock } from 'vitest-auto-spy';\n\ninterface UserService {\n  getName(id: number): string;\n  getUser(id: number): Promise\u003cUser\u003e;\n  apiUrl: string;\n}\n\n// Before — needs a concrete class:\n// const svc = createSpyFromClass(UserServiceClass);\n\n// After — type only, no class:\nconst svc = createAutoMock\u003cUserService\u003e();\n```\n\nEvery accessed method becomes a decorated spy with the **same typed control helpers** as\n`createSpyFromClass`, materialized lazily and cached (same reference on re-access):\n\n```ts\nsvc.getName.calledWith(1).mockReturnValue('Ada');   // sync, arg-matched\nsvc.getUser.resolveWith({ id: 1, name: 'Ada' });    // promise helper\nexpect(svc.getName(1)).toBe('Ada');\nawait expect(svc.getUser(1)).resolves.toEqual({ id: 1, name: 'Ada' });\n```\n\nSeed concrete values or implementations with the optional `overrides` argument (seeded keys are\nreturned as-is, never turned into spies):\n\n```ts\nconst svc = createAutoMock\u003cUserService\u003e({ apiUrl: 'https://api.test' });\nexpect(svc.apiUrl).toBe('https://api.test'); // or assign: svc.apiUrl = '...'\n```\n\n\u003e Caveat: with only a type at runtime, methods and plain properties are indistinguishable on\n\u003e access — an un-seeded property read returns a spy. Seed real property values via `overrides`\n\u003e (or assignment) to get them back verbatim.\n\n## Synchronous methods\n\n```ts\n// standard vi.fn() API works as-is\nmyService.getName.mockReturnValue('Fake Name');\n\n// return a value only for specific arguments\nmyService.getName.calledWith(1).mockReturnValue('Fake Name');\nexpect(myService.getName(1)).toBe('Fake Name');\nexpect(myService.getName(2)).toBeUndefined();\n\n// throw if called with the \"wrong\" arguments\nmyService.getName.mustBeCalledWith(1).mockReturnValue('Fake Name');\nexpect(() =\u003e myService.getName(2)).toThrow();\n```\n\n## Promise-returning methods\n\n```ts\nmyService.getProducts.resolveWith([{ name: 'Product 1' }]);\nawait expect(myService.getProducts()).resolves.toEqual([{ name: 'Product 1' }]);\n\nmyService.getProducts.rejectWith('FAKE ERROR');\nawait expect(myService.getProducts()).rejects.toBe('FAKE ERROR');\n\n// per-call values, and conditional-by-args\nmyService.getProducts.resolveWithPerCall([{ value: ['a'] }, { value: ['b'] }]);\nmyService.getProducts.calledWith(1).resolveWith(['one']);\n```\n\n## Observable-returning methods \u0026 Observable properties\n\nBoth spied **methods** that return an `Observable` and spied **properties** of type\n`Observable` get the same control surface. Enable them by importing the rxjs layer once:\n\n```ts\nimport 'vitest-auto-spy/rxjs';\n```\n\n```ts\nmyService.getProducts$.nextWith([{ name: 'Product 1' }]); // emit, stream stays open\nmyService.getProducts$.nextOneTimeWith([{ name: 'X' }]);  // emit one value, then complete\nmyService.getProducts$.throwWith('FAKE ERROR');           // error the stream\nmyService.getProducts$.complete();                        // complete the stream\n\n// emit a precise sequence — values, errors, completion, optional delays\nmyService.getProducts$.nextWithValues([\n  { value: [{ name: 'Product 1' }] },\n  { errorValue: 'FAKE ERROR' },\n  { complete: true },\n]);\n\n// a fresh stream per call\nmyService.getProducts$.nextWithPerCall([{ value: ['a'] }, { value: ['b'] }]);\n\n// grab the underlying Subject for full manual control\nconst subject = myService.getProducts$.returnSubject();\nsubject.next([{ name: 'manual' }]);\n```\n\n`calledWith(...)` / `mustBeCalledWith(...)` also chain into the observable helpers:\n\n```ts\nmyService.getProducts$.calledWith(1).nextWith([{ name: 'Product 1' }]);\n```\n\n### Standalone observable builder\n\n```ts\nimport { createObservableWithValues } from 'vitest-auto-spy/rxjs';\n\nconst fake$ = createObservableWithValues([{ value: 1 }, { value: 2 }, { complete: true }]);\n\n// or get the subject too\nconst { values$, subject } = createObservableWithValues([{ value: 1 }], { returnSubject: true });\n```\n\n## Getters \u0026 setters\n\n```ts\nconst spy = createSpyFromClass(MyService, {\n  gettersToSpyOn: ['userName'],\n  settersToSpyOn: ['userName'],\n});\n\n// configure / assert the getter\nspy.accessorSpies.getters.userName.mockReturnValue('Fake Name');\nexpect(spy.userName).toBe('Fake Name');\n\n// assert the setter was called\nspy.userName = 'New Name';\nexpect(spy.accessorSpies.setters.userName).toHaveBeenCalledWith('New Name');\n```\n\n## Framework adapters\n\nThe core is framework-agnostic — `createSpyFromClass` / `createAutoMock` work in any test. The\nsubpaths below add a natural import and, where the framework has class DI, a tiny `provide*` helper.\nNone of them pull the framework into this package; they're recipes over the same core.\n\n\u003e The **Angular**, **NestJS**, **React**, **Vue/Pinia** and **Svelte** entry points are all published\n\u003e ([Availability](#availability)). Each is a thin recipe over the same core, so you can equally copy it\n\u003e using the core `vitest-auto-spy` import directly.\n\n### NestJS\n\nUse `provideAutoSpy` to register a fully-mocked service in a `TestingModule`, then `injectSpy` to\npull it back out already typed as `Spy\u003cT\u003e`. `@nestjs/common` / `@nestjs/testing` are your own\n(optional) peers — the helper imports neither:\n\n```ts\nimport { Test, type TestingModule } from '@nestjs/testing';\nimport { provideAutoSpy, injectSpy } from 'vitest-auto-spy/nestjs';\nimport { beforeEach, expect, it } from 'vitest';\n\nimport { AuthService } from './auth.service';\nimport { UserService } from './user.service';\n\nlet moduleRef: TestingModule;\nlet userServiceSpy: Spy\u003cUserService\u003e;\n\nbeforeEach(async () =\u003e {\n  moduleRef = await Test.createTestingModule({\n    providers: [AuthService, provideAutoSpy(UserService)],\n  }).compile();\n\n  userServiceSpy = injectSpy(moduleRef, UserService);\n});\n\nit('logs in a known user', () =\u003e {\n  userServiceSpy.findByEmail.mockReturnValue({ id: 1, name: 'Ada' });\n\n  const auth = moduleRef.get(AuthService);\n  expect(auth.login('ada@example.com')).toBeTruthy();\n  expect(userServiceSpy.findByEmail).toHaveBeenCalledWith('ada@example.com');\n});\n```\n\n### React (Testing Library)\n\nReact has no DI container, so there's no `provide*` helper — the recipe is: **spy the classes you\nown** (services, stores, API clients, hook deps), then pass the spy into a Context provider or hook.\nThe spy is a plain object of spied functions, so it drops straight into `value={...}`:\n\n```tsx\nimport { render, screen } from '@testing-library/react';\nimport { createSpyFromClass, type Spy } from 'vitest-auto-spy/react';\nimport { CartContext, Cart } from './cart';\n\nclass CartStore {\n  getItemCount(): number { return 0; }\n  checkout(token: string): Promise\u003c{ orderId: string }\u003e { /* ... */ }\n}\n\nlet cart: Spy\u003cCartStore\u003e;\n\nbeforeEach(() =\u003e {\n  cart = createSpyFromClass(CartStore); // every method is now a spy\n});\n\nit('shows the item count from the injected store', () =\u003e {\n  cart.getItemCount.mockReturnValue(3);\n\n  render(\n    \u003cCartContext.Provider value={cart}\u003e\n      \u003cCart /\u003e\n    \u003c/CartContext.Provider\u003e,\n  );\n\n  expect(screen.getByText('3 items')).toBeInTheDocument();\n});\n\nit('drives async deps and asserts the component called them', async () =\u003e {\n  cart.checkout.resolveWith({ orderId: 'ord_42' });\n  // ...trigger checkout in the UI...\n  expect(cart.checkout).toHaveBeenCalledWith('tok_abc');\n});\n```\n\n### Vue / Pinia\n\n`provideAutoSpy(token, Class)` returns a `{ [token]: Spy\u003cT\u003e }` map you can spread into\n`@vue/test-utils`' `global.provide`; for a class-based Pinia store, spy it directly:\n\n```ts\nimport { mount } from '@vue/test-utils';\nimport { createSpyFromClass, provideAutoSpy } from 'vitest-auto-spy/vue';\n\n// (a) class-based service injected via provide / global.provide\nimport { UserServiceKey, UserService } from '@/services/user.service';\n\nconst provide = provideAutoSpy(UserServiceKey, UserService); // { [UserServiceKey]: Spy\u003cUserService\u003e }\nprovide[UserServiceKey].getName.mockReturnValue('Fake Name');\n\nconst wrapper = mount(UserBadge, { global: { provide } });\nexpect(provide[UserServiceKey].getName).toHaveBeenCalled();\n\n// (b) class-based Pinia store — every action becomes a spy\nimport { CartStore } from '@/stores/cart.store';\n\nconst store = createSpyFromClass(CartStore);\nstore.itemCount.mockReturnValue(3);                  // sync action/getter\nstore.checkout.resolveWith({ orderId: 'ord_42' });   // async action (Promise)\nawait store.checkout('tok_abc');\nexpect(store.checkout).toHaveBeenCalledWith('tok_abc');\n```\n\n### Svelte\n\nSvelte has no class-based DI, so it's a recipe: keep your logic in plain class-based\nservices/stores, spy the class, and hand the spy to the component the same way it receives the real\none (props, context, or a mocked module):\n\n```ts\nimport { render } from '@testing-library/svelte';\nimport { createSpyFromClass } from 'vitest-auto-spy/svelte';\nimport Cart from './Cart.svelte';\nimport { CartStore } from './cart-store';\n\nit('shows the cart total from the store', () =\u003e {\n  const cartStore = createSpyFromClass(CartStore); // every method is a spy\n\n  cartStore.total.mockReturnValue(42);\n  cartStore.priceOf.calledWith('apple').mockReturnValue(7);\n\n  render(Cart, { props: { store: cartStore } });\n\n  expect(cartStore.total).toHaveBeenCalled();\n});\n```\n\n### Angular\n\n`provideAutoSpy` is the shorthand for providing an auto-spy in a `TestBed`:\n\n```ts\nimport { provideAutoSpy, injectSpy } from 'vitest-auto-spy/angular';\n\nTestBed.configureTestingModule({\n  providers: [\n    provideAutoSpy(MyService),\n    // accepts the same second argument as createSpyFromClass\n    provideAutoSpy(ApiService, { methodsToSpyOn: ['get', 'post'] }),\n  ],\n});\n\nlet myService: Spy\u003cMyService\u003e;\n\nbeforeEach(() =\u003e {\n  myService = injectSpy(MyService);\n});\n```\n\n\u003e The spies are change-detection agnostic, so they work in **both zoneless and\n\u003e zone.js** Angular projects — nothing here touches `NgZone` or change detection.\n\u003e You only need the usual Vitest + Angular wiring:\n\u003e [`@analogjs/vite-plugin-angular`](https://www.npmjs.com/package/@analogjs/vite-plugin-angular)\n\u003e plus a TestBed setup file (e.g. `@analogjs/vitest-angular`'s `setupTestBed()`).\n\n#### Signal / readonly property mocking (bonus)\n\n```ts\nimport { mockReadonlyProp, mockReadonlyPropGetter, mockAccessorsProp } from 'vitest-auto-spy/angular';\n\nmockReadonlyProp(service, 'isReady', true);              // static value (incl. signals)\nmockReadonlyPropGetter(service, 'label', () =\u003e 'A');     // dynamic getter\nmockAccessorsProp(service, 'theme');                     // spied get + set\n```\n\n## Utilities\n\nBeyond the spy factories, the package ships a set of small standalone helpers. Each one is a\nsingle-purpose utility you can pick up independently — they all ride on the same core:\n\n| Utility | Entry point | What it's for |\n| --- | --- | --- |\n| `injectSpy(token)` / `injectSpy(moduleRef, token)` | `/angular`, `/nestjs` | Pull a provided spy out of the DI container, already typed as `Spy\u003cT\u003e` — no casting |\n| `provideAutoSpy(Class, config?)` | `/angular`, `/nestjs`, `/vue` | One-liner `{ provide, useValue }` (or Vue `global.provide`) that builds the spy for you |\n| `createFunctionSpy(name)` | core | A single standalone function spy with the full helper set (`calledWith`, `resolveWith`, `nextWith`, …) — no class needed |\n| `createAutoMock\u003cT\u003e(overrides?)` | core | Proxy-based spy from a **type/interface** alone ([details](#auto-mock-by-type-no-class-needed)) |\n| `createObservableWithValues(configs, opts?)` | `/rxjs` | Build a fake `Observable` emitting a precise sequence of values / errors / completion |\n| `consoleInfoSpy` / `consoleWarnSpy` / … | `/console` | Silent typed spies over the global `console`, installed on import ([details](#console-spies--vitest-auto-spyconsole)) |\n| `mockReadonlyProp(obj, prop, value)` | `/angular` | Overwrite a `readonly` property (incl. Angular signals) with a static value |\n| `mockReadonlyPropGetter(obj, prop, getter)` | `/angular` | Same, but backed by a dynamic getter |\n| `mockAccessorsProp(obj, prop)` | `/angular` | Redefine a property with spied `get` + `set` |\n| `errorHandler` | core | The `mustBeCalledWith` argument-mismatch reporter — swap it to customize failure output |\n\nA taste of the DI pair — provide the spy, inject it back fully typed:\n\n```ts\nimport { provideAutoSpy, injectSpy } from 'vitest-auto-spy/angular';\n\nTestBed.configureTestingModule({ providers: [provideAutoSpy(UserService)] });\nconst userService = injectSpy(UserService); // Spy\u003cUserService\u003e, no `as` cast\n```\n\nAnd a standalone function spy, when there's no class or interface at all:\n\n```ts\nimport { createFunctionSpy } from 'vitest-auto-spy';\n\nconst onSave = createFunctionSpy\u003c(id: number) =\u003e Promise\u003cvoid\u003e\u003e('onSave');\nonSave.calledWith(1).resolveWith();\n```\n\n### Console spies — `vitest-auto-spy/console`\n\nImporting the entry replaces `console.debug` / `error` / `info` / `log` / `time` / `timeEnd` /\n`trace` / `warn` with **silent, fully-typed spies** and exports each one ready to assert — no\n`vi.spyOn(console, 'info')` boilerplate in every suite, no log output polluting the test run:\n\n```ts\nimport { consoleInfoSpy, consoleWarnSpy } from 'vitest-auto-spy/console';\n\nservice.doWork();\n\nexpect(consoleInfoSpy).toHaveBeenCalledWith('done');\nexpect(consoleWarnSpy).not.toHaveBeenCalled();\n```\n\nHousekeeping: `resetConsoleSpies()` clears the recorded calls between tests (Vitest's\n`clearMocks: true` already does that automatically), `restoreConsole()` puts the original\nmethods back, and `installConsoleSpies()` re-installs after a restore.\n\n\u003e The spies use the registered `MockAdapter` — import your runtime entry\n\u003e (`vitest-auto-spy/bun`, `…/node`) **before** `vitest-auto-spy/console` and the console spies\n\u003e are driven by that runner's mocks; with no prior runtime entry the default Vitest adapter is used.\n\nPrefer a fully detached fake instead of touching the real global? `createAutoMock\u003cConsole\u003e()`\ngives you a typed, in-memory console to inject into code that takes a logger:\n\n```ts\nimport { createAutoMock } from 'vitest-auto-spy';\n\nconst fakeConsole = createAutoMock\u003cConsole\u003e();\nconst service = new ReportService(fakeConsole);\n\nservice.doWork();\n\nexpect(fakeConsole.info).toHaveBeenCalledWith('done');\n```\n\n## API reference\n\n| Export | Description |\n| --- | --- |\n| `createSpyFromClass(Class, methodsOrConfig?)` | Build a fully-typed `Spy\u003cT\u003e` from a class |\n| `createAutoMock\u003cT\u003e(overrides?)` | Build a `Spy\u003cT\u003e` from a **type/interface** alone (Proxy, no class) |\n| `provideAutoSpy(Class, methodsOrConfig?)` | Angular / NestJS `{ provide, useValue }` shorthand |\n| `provideAutoSpy(token, Class, methodsOrConfig?)` | Vue `{ [token]: Spy\u003cT\u003e }` for `global.provide` |\n| `injectSpy(token)` _(Angular)_ / `injectSpy(moduleRef, token)` _(NestJS)_ | Inject typed as `Spy\u003cT\u003e` |\n| `createFunctionSpy(name)` | A single standalone function spy with all helpers |\n| `createObservableWithValues(configs, opts?)` | Build an Observable from value configs |\n| `mockReadonlyProp` / `mockReadonlyPropGetter` / `mockAccessorsProp` | Mock readonly / accessor / signal props |\n| `consoleDebugSpy` … `consoleWarnSpy` _(`/console`)_ | Silent typed spies replacing the global `console` methods on import |\n| `installConsoleSpies()` / `resetConsoleSpies()` / `restoreConsole()` | Install / clear / undo the console spies |\n| `errorHandler` | The `mustBeCalledWith` argument-mismatch error helper |\n\n**Spied sync method:** `mockReturnValue`, `calledWith(...)`, `mustBeCalledWith(...)`\n\n**Spied Promise method:** `resolveWith`, `rejectWith`, `resolveWithPerCall`\n\n**Spied Observable method / property:** `nextWith`, `nextOneTimeWith`, `nextWithValues`,\n`nextWithPerCall`, `throwWith`, `complete`, `returnSubject`\n\n**Config (`ClassSpyConfiguration`):** `methodsToSpyOn`, `observablePropsToSpyOn`,\n`gettersToSpyOn`, `settersToSpyOn`\n\n`ValueConfig` (for `nextWithValues`): `{ value, delay? }` | `{ errorValue, delay? }` | `{ complete?, delay? }`.\n\n## FAQ \u0026 troubleshooting\n\n**\"I get `X.nextWith is not a function` / observable helpers are missing.\"**\nImport the rxjs layer once (e.g. in your test setup): `import 'vitest-auto-spy/rxjs';`. Without it,\nrequesting an observable spy throws a hint pointing you here.\n\n**\"My method isn't on the spy.\"**\nAuto-discovery only sees **prototype methods**. Arrow-function class fields (`foo = () =\u003e {}`) and\nplain properties aren't included — see [How it works](#how-it-works-and-what-it-wont-spy). List\ngetters/setters via `gettersToSpyOn` / `settersToSpyOn`.\n\n**\"Does it construct my class? Will the constructor's side effects run?\"**\nNo. `createSpyFromClass` reads the prototype and never `new`s the class, so constructors (and their\nHTTP/DB/`inject()` side effects) never run.\n\n**\"I only have an interface/type, not a class.\"**\nUse [`createAutoMock\u003cT\u003e()`](#auto-mock-by-type-no-class-needed) — it builds the spy lazily from the\ntype via a `Proxy`, no runtime class needed.\n\n**\"Can I use it without TypeScript?\"**\nYes — the runtime works in plain JS; you just lose the compile-time `Spy\u003cT\u003e` typing.\n\n**\"Native mock methods differ between runners.\"**\nOnly the auto-spy helpers are normalised. Native APIs stay the runner's own (`mockReturnValue` on\nVitest/Bun, `spy.method.mock.mockImplementation` on `node:test`).\n\n## Versioning\n\nThis package follows [Semantic Versioning](https://semver.org). Breaking changes to the public API\nland only in major releases; see the [Changelog](./CHANGELOG.md) for what changed in each version.\nReleases are automated from Conventional Commits (see [Contributing](#contributing)).\n\n## Contributing\n\nContributions are welcome! Please read [CONTRIBUTING.md](./CONTRIBUTING.md) and the\n[Code of Conduct](./CODE_OF_CONDUCT.md). In short:\n\n```bash\nnpm ci\nnpm test            # run the suite\nnpm run test:coverage   # 100% thresholds enforced\nnpm run build\n```\n\nReleases are automated: merging a PR into `master` bumps the version from the\nConventional Commit types and publishes to npm — see\n[CONTRIBUTING.md → Releasing](./CONTRIBUTING.md#releasing).\n\nIf this package saved you time, a ⭐ on [GitHub](https://github.com/ASDAlexey/vitest-auto-spy)\nhelps others find it.\n\n## Acknowledgements\n\nAPI and ergonomics are modelled on Shai Reznik's\n[`jest-auto-spies`](https://www.npmjs.com/package/jest-auto-spies) — `vitest-auto-spy` is its\nVitest-era successor with the same surface, so migrations are (mostly) a find-and-replace. Thanks to\nthe Vitest, Bun, RxJS and Angular communities whose tooling this builds on.\n\n## License\n\n[MIT](./LICENSE) © [Alexey Popov](https://github.com/ASDAlexey)\n\nGet in touch: [asdalexey.github.io](https://asdalexey.github.io/ru/)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FASDAlexey%2Fvitest-auto-spy","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FASDAlexey%2Fvitest-auto-spy","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FASDAlexey%2Fvitest-auto-spy/lists"}