{"id":15825392,"url":"https://github.com/mcous/vitest-when","last_synced_at":"2025-04-06T06:07:35.748Z","repository":{"id":163722156,"uuid":"639178317","full_name":"mcous/vitest-when","owner":"mcous","description":"Stub behaviors of Vitest mock functions based on how they are called","archived":false,"fork":false,"pushed_at":"2025-02-18T02:47:03.000Z","size":182,"stargazers_count":34,"open_issues_count":0,"forks_count":4,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-29T03:25:51.703Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","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/mcous.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-05-10T23:46:30.000Z","updated_at":"2025-03-08T09:14:49.000Z","dependencies_parsed_at":"2023-11-29T05:25:39.252Z","dependency_job_id":"d3088eef-089c-4d6e-ade7-50fc6b0ec13e","html_url":"https://github.com/mcous/vitest-when","commit_stats":{"total_commits":31,"total_committers":2,"mean_commits":15.5,"dds":"0.32258064516129037","last_synced_commit":"d7b7234e5a891a6c4fe3f68c05b6afa0b870c40e"},"previous_names":[],"tags_count":10,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mcous%2Fvitest-when","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mcous%2Fvitest-when/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mcous%2Fvitest-when/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mcous%2Fvitest-when/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mcous","download_url":"https://codeload.github.com/mcous/vitest-when/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246774212,"owners_count":20831494,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":[],"created_at":"2024-10-05T09:09:07.947Z","updated_at":"2025-04-06T06:07:35.718Z","avatar_url":"https://github.com/mcous.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# vitest-when\n\n[![npm badge][]][npm]\n[![ci badge][]][ci]\n[![coverage badge][]][coverage]\n\nRead the introductory post: [Better mocks in Vitest][better-mocks]\n\nStub behaviors of [Vitest][] mock functions with a small, readable API. Inspired by [testdouble.js][] and [jest-when][].\n\n```shell\nnpm install --save-dev vitest-when\n```\n\n[better-mocks]: https://michael.cousins.io/articles/2023-06-30-better-stubs/\n[vitest]: https://vitest.dev/\n[testdouble.js]: https://github.com/testdouble/testdouble.js/\n[jest-when]: https://github.com/timkindberg/jest-when\n[npm]: https://npmjs.org/vitest-when\n[npm badge]: https://img.shields.io/npm/v/vitest-when.svg?style=flat-square\n[ci]: https://github.com/mcous/vitest-when/actions\n[ci badge]: https://img.shields.io/github/actions/workflow/status/mcous/vitest-when/ci.yaml?style=flat-square\n[coverage]: https://coveralls.io/github/mcous/vitest-when\n[coverage badge]: https://img.shields.io/coverallsCoverage/github/mcous/vitest-when?style=flat-square\n\n## Usage\n\nCreate [stubs][] - fake objects that have pre-configured responses to matching arguments - from [Vitest's mock functions][]. With vitest-when, your stubs are:\n\n- Easy to read\n- Hard to misconfigure, especially when using TypeScript\n\nWrap your `vi.fn()` mock - or a function imported from a `vi.mock`'d module - in [`when`][when], match on a set of arguments using [`calledWith`][called-with], and configure a behavior\n\n- [`.thenReturn()`][then-return] - Return a value\n- [`.thenResolve()`][then-resolve] - Resolve a `Promise`\n- [`.thenThrow()`][then-throw] - Throw an error\n- [`.thenReject()`][then-reject] - Reject a `Promise`\n- [`.thenDo()`][then-do] - Trigger a function\n\nIf the stub is called with arguments that match `calledWith`, the configured behavior will occur. If the arguments do not match, the stub will no-op and return `undefined`.\n\n```ts\nimport { vi, test, afterEach } from 'vitest'\nimport { when } from 'vitest-when'\n\nafterEach(() =\u003e {\n  vi.resetAllMocks()\n})\n\ntest('stubbing with vitest-when', () =\u003e {\n  const stub = vi.fn()\n\n  when(stub).calledWith(1, 2, 3).thenReturn(4)\n  when(stub).calledWith(4, 5, 6).thenReturn(7)\n\n  let result = stub(1, 2, 3)\n  expect(result).toBe(4)\n\n  result = stub(4, 5, 6)\n  expect(result).toBe(7)\n\n  result = stub(7, 8, 9)\n  expect(result).toBe(undefined)\n})\n```\n\nYou should call `vi.resetAllMocks()` in your suite's `afterEach` hook to remove the implementation added by `when`. You can also set Vitest's [`mockReset`](https://vitest.dev/config/#mockreset) config to `true` instead of using `afterEach`.\n\n[vitest's mock functions]: https://vitest.dev/api/mock.html\n[stubs]: https://en.wikipedia.org/wiki/Test_stub\n[when]: #whenspy-tfunc-stubwrappertfunc\n[called-with]: #calledwithargs-targs-stubtargs-treturn\n[then-return]: #thenreturnvalue-treturn\n[then-resolve]: #thenresolvevalue-treturn\n[then-throw]: #thenthrowerror-unknown\n[then-reject]: #thenrejecterror-unknown\n[then-do]: #thendocallback-args-targs--treturn\n\n### Why not vanilla Vitest mocks?\n\nVitest's mock functions are powerful, but have an overly permissive API, inherited from Jest. Vanilla `vi.fn()` mock functions are difficult to use well and easy to use poorly.\n\n- Mock usage is spread across the [arrange and assert][] phases of your test, with \"act\" in between, making the test harder to read.\n- If you forget the `expect(...).toHaveBeenCalledWith(...)` step, the test will pass even if the mock is called incorrectly.\n- `expect(...).toHaveBeenCalledWith(...)` is not type-checked, as of Vitest `0.31.0`.\n\n```ts\n// arrange\nconst stub = vi.fn()\nstub.mockReturnValue('world')\n\n// act\nconst result = stub('hello')\n\n// assert\nexpect(stub).toHaveBeenCalledWith('hello')\nexpect(result).toBe('world')\n```\n\nIn contrast, when using vitest-when stubs:\n\n- All stub configuration happens in the \"arrange\" phase of your test.\n- You cannot forget `calledWith`.\n- `calledWith` and `thenReturn` (et. al.) are fully type-checked.\n\n```ts\n// arrange\nconst stub = vi.fn()\nwhen(stub).calledWith('hello').thenReturn('world')\n\n// act\nconst result = stub('hello')\n\n// assert\nexpect(result).toBe('world')\n```\n\n[arrange and assert]: https://github.com/testdouble/contributing-tests/wiki/Arrange-Act-Assert\n\n### Example\n\nSee the [./example](./example) directory for example usage.\n\n```ts\n// meaning-of-life.test.ts\nimport { vi, describe, afterEach, it, expect } from 'vitest'\nimport { when } from 'vitest-when'\n\nimport * as deepThought from './deep-thought.ts'\nimport * as earth from './earth.ts'\nimport * as subject from './meaning-of-life.ts'\n\nvi.mock('./deep-thought.ts')\nvi.mock('./earth.ts')\n\ndescribe('get the meaning of life', () =\u003e {\n  afterEach(() =\u003e {\n    vi.resetAllMocks()\n  })\n\n  it('should get the answer and the question', async () =\u003e {\n    when(deepThought.calculateAnswer).calledWith().thenResolve(42)\n    when(earth.calculateQuestion).calledWith(42).thenResolve(\"What's 6 by 9?\")\n\n    const result = await subject.createMeaning()\n\n    expect(result).toEqual({ question: \"What's 6 by 9?\", answer: 42 })\n  })\n})\n```\n\n```ts\n// meaning-of-life.ts\nimport { calculateAnswer } from './deep-thought.ts'\nimport { calculateQuestion } from './earth.ts'\n\nexport interface Meaning {\n  question: string\n  answer: number\n}\n\nexport const createMeaning = async (): Promise\u003cMeaning\u003e =\u003e {\n  const answer = await calculateAnswer()\n  const question = await calculateQuestion(answer)\n\n  return { question, answer }\n}\n```\n\n```ts\n// deep-thought.ts\nexport const calculateAnswer = async (): Promise\u003cnumber\u003e =\u003e {\n  throw new Error(`calculateAnswer() not implemented`)\n}\n```\n\n```ts\n// earth.ts\nexport const calculateQuestion = async (answer: number): Promise\u003cstring\u003e =\u003e {\n  throw new Error(`calculateQuestion(${answer}) not implemented`)\n}\n```\n\n## API\n\n### `when(spy: TFunc, options?: WhenOptions): StubWrapper\u003cTFunc\u003e`\n\nConfigures a `vi.fn()` or `vi.spyOn()` mock function to act as a vitest-when stub. Adds an implementation to the function that initially no-ops, and returns an API to configure behaviors for given arguments using [`.calledWith(...)`][called-with]\n\n```ts\nimport { vi } from 'vitest'\nimport { when } from 'vitest-when'\n\nconst spy = vi.fn()\n\nwhen(spy)\n\nexpect(spy()).toBe(undefined)\n```\n\n#### Options\n\n```ts\nimport type { WhenOptions } from 'vitest-when'\n```\n\n| option  | default | type    | description                                        |\n| ------- | ------- | ------- | -------------------------------------------------- |\n| `times` | N/A     | integer | Only trigger configured behavior a number of times |\n\n### `.calledWith(...args: TArgs): Stub\u003cTArgs, TReturn\u003e`\n\nCreate a stub that matches a given set of arguments which you can configure with different behaviors using methods like [`.thenReturn(...)`][then-return].\n\n```ts\nconst spy = vi.fn()\n\nwhen(spy).calledWith('hello').thenReturn('world')\n\nexpect(spy('hello')).toEqual('world')\n```\n\nWhen a call to a mock uses arguments that match those given to `calledWith`, a configured behavior will be triggered. All arguments must match, but you can use Vitest's [asymmetric matchers][] to loosen the stubbing:\n\n```ts\nconst spy = vi.fn()\n\nwhen(spy).calledWith(expect.any(String)).thenReturn('world')\n\nexpect(spy('hello')).toEqual('world')\nexpect(spy('anything')).toEqual('world')\n```\n\nIf `calledWith` is used multiple times, the last configured stubbing will be used.\n\n```ts\nwhen(spy).calledWith('hello').thenReturn('world')\nexpect(spy('hello')).toEqual('world')\nwhen(spy).calledWith('hello').thenReturn('goodbye')\nexpect(spy('hello')).toEqual('goodbye')\n```\n\n[asymmetric matchers]: https://vitest.dev/api/expect.html#expect-anything\n\n#### Types of overloaded functions\n\nDue to fundamental limitations in TypeScript, `when()` will always use the _last_ overload to infer function parameters and return types. You can use the `TFunc` type parameter of `when()` to manually select a different overload entry:\n\n```ts\nfunction overloaded(): null\nfunction overloaded(input: number): string\nfunction overloaded(input?: number): string | null {\n  // ...\n}\n\n// Last entry: all good!\nwhen(overloaded).calledWith(42).thenReturn('hello')\n\n// $ts-expect-error: first entry\nwhen(overloaded).calledWith().thenReturn(null)\n\n// Manually specified: all good!\nwhen\u003c() =\u003e null\u003e(overloaded).calledWith().thenReturn(null)\n```\n\n#### Fallback\n\nBy default, if arguments do not match, a vitest-when stub will no-op and return `undefined`. You can customize this fallback by configuring your own unconditional behavior on the mock using Vitest's built-in [mock API][].\n\n```ts\nconst spy = vi.fn().mockReturnValue('you messed up!')\n\nwhen(spy).calledWith('hello').thenReturn('world')\n\nspy('hello') // \"world\"\nspy('jello') // \"you messed up!\"\n```\n\n[mock API]: https://vitest.dev/api/mock.html\n\n### `.thenReturn(value: TReturn)`\n\nWhen the stubbing is satisfied, return `value`\n\n```ts\nconst spy = vi.fn()\n\nwhen(spy).calledWith('hello').thenReturn('world')\n\nexpect(spy('hello')).toEqual('world')\n```\n\nTo only return a value once, use the `times` option.\n\n```ts\nimport { when } from 'vitest-when'\n\nconst spy = vi.fn()\n\nwhen(spy, { times: 1 }).calledWith('hello').thenReturn('world')\n\nexpect(spy('hello')).toEqual('world')\nexpect(spy('hello')).toEqual(undefined)\n```\n\nYou may pass several values to `thenReturn` to return different values in succession. If you do not specify `times`, the last value will be latched. Otherwise, each value will be returned the specified number of times.\n\n```ts\nconst spy = vi.fn()\n\nwhen(spy).calledWith('hello').thenReturn('hi', 'sup?')\n\nexpect(spy('hello')).toEqual('hi')\nexpect(spy('hello')).toEqual('sup?')\nexpect(spy('hello')).toEqual('sup?')\n```\n\n### `.thenResolve(value: TReturn)`\n\nWhen the stubbing is satisfied, resolve a `Promise` with `value`\n\n```ts\nconst spy = vi.fn()\n\nwhen(spy).calledWith('hello').thenResolve('world')\n\nexpect(await spy('hello')).toEqual('world')\n```\n\nTo only resolve a value once, use the `times` option.\n\n```ts\nimport { when } from 'vitest-when'\n\nconst spy = vi.fn()\n\nwhen(spy, { times: 1 }).calledWith('hello').thenResolve('world')\n\nexpect(await spy('hello')).toEqual('world')\nexpect(spy('hello')).toEqual(undefined)\n```\n\nYou may pass several values to `thenResolve` to resolve different values in succession. If you do not specify `times`, the last value will be latched. Otherwise, each value will be resolved the specified number of times.\n\n```ts\nconst spy = vi.fn()\n\nwhen(spy).calledWith('hello').thenResolve('hi', 'sup?')\n\nexpect(await spy('hello')).toEqual('hi')\nexpect(await spy('hello')).toEqual('sup?')\nexpect(await spy('hello')).toEqual('sup?')\n```\n\n### `.thenThrow(error: unknown)`\n\nWhen the stubbing is satisfied, throw `error`.\n\n```ts\nconst spy = vi.fn()\n\nwhen(spy).calledWith('hello').thenThrow(new Error('oh no'))\n\nexpect(() =\u003e spy('hello')).toThrow('oh no')\n```\n\nTo only throw an error only once, use the `times` option.\n\n```ts\nimport { when } from 'vitest-when'\n\nconst spy = vi.fn()\n\nwhen(spy, { times: 1 }).calledWith('hello').thenThrow(new Error('oh no'))\n\nexpect(() =\u003e spy('hello')).toThrow('oh no')\nexpect(spy('hello')).toEqual(undefined)\n```\n\nYou may pass several values to `thenThrow` to throw different errors in succession. If you do not specify `times`, the last value will be latched. Otherwise, each error will be thrown the specified number of times.\n\n```ts\nconst spy = vi.fn()\n\nwhen(spy)\n  .calledWith('hello')\n  .thenThrow(new Error('oh no'), new Error('this is bad'))\n\nexpect(() =\u003e spy('hello')).toThrow('oh no')\nexpect(() =\u003e spy('hello')).toThrow('this is bad')\nexpect(() =\u003e spy('hello')).toThrow('this is bad')\n```\n\n### `.thenReject(error: unknown)`\n\nWhen the stubbing is satisfied, reject a `Promise` with `error`.\n\n```ts\nconst spy = vi.fn()\n\nwhen(spy).calledWith('hello').thenReject(new Error('oh no'))\n\nawait expect(spy('hello')).rejects.toThrow('oh no')\n```\n\nTo only throw an error only once, use the `times` option.\n\n```ts\nimport { times, when } from 'vitest-when'\n\nconst spy = vi.fn()\n\nwhen(spy, { times: 1 }).calledWith('hello').thenReject(new Error('oh no'))\n\nawait expect(spy('hello')).rejects.toThrow('oh no')\nexpect(spy('hello')).toEqual(undefined)\n```\n\nYou may pass several values to `thenReject` to throw different errors in succession. If you do not specify `times`, the last value will be latched. Otherwise, each rejection will be triggered the specified number of times.\n\n```ts\nconst spy = vi.fn()\n\nwhen(spy)\n  .calledWith('hello')\n  .thenReject(new Error('oh no'), new Error('this is bad'))\n\nawait expect(spy('hello')).rejects.toThrow('oh no')\nawait expect(spy('hello')).rejects.toThrow('this is bad')\nawait expect(spy('hello')).rejects.toThrow('this is bad')\n```\n\n### `.thenDo(callback: (...args: TArgs) =\u003e TReturn)`\n\nWhen the stubbing is satisfied, run `callback` to trigger a side-effect and return its result (if any). `thenDo` is a relatively powerful tool for stubbing complex behaviors, so if you find yourself using `thenDo` often, consider refactoring your code to use more simple interactions! Your future self will thank you.\n\n```ts\nconst spy = vi.fn()\nlet called = false\n\nwhen(spy)\n  .calledWith('hello')\n  .thenDo(() =\u003e {\n    called = true\n    return 'world'\n  })\n\nexpect(spy('hello')).toEqual('world')\nexpect(called).toEqual(true)\n```\n\nTo only run the callback once, use the `times` option.\n\n```ts\nimport { times, when } from 'vitest-when'\n\nconst spy = vi.fn()\n\nwhen(spy, { times: 1 })\n  .calledWith('hello')\n  .thenDo(() =\u003e 'world')\n\nexpect(spy('hello')).toEqual('world')\nexpect(spy('hello')).toEqual(undefined)\n```\n\nYou may pass several callbacks to `thenDo` to trigger different side-effects in succession. If you do not specify `times`, the last callback will be latched. Otherwise, each callback will be triggered the specified number of times.\n\n```ts\nconst spy = vi.fn()\n\nwhen(spy)\n  .calledWith('hello')\n  .thenDo(\n    () =\u003e 'world',\n    () =\u003e 'solar system',\n  )\n\nexpect(spy('hello')).toEqual('world')\nexpect(spy('hello')).toEqual('solar system')\n```\n\n### `debug(spy: TFunc, options?: DebugOptions): DebugResult`\n\nLogs and returns information about a mock's stubbing and usage. Useful if a test with mocks is failing and you can't figure out why.\n\n```ts\nimport { when, debug } from 'vitest-when'\n\nconst coolFunc = vi.fn().mockName('coolFunc')\n\nwhen(coolFunc).calledWith(1, 2, 3).thenReturn(123)\nwhen(coolFunc).calledWith(4, 5, 6).thenThrow(new Error('oh no'))\n\nconst result = coolFunc(1, 2, 4)\n\ndebug(coolFunc)\n// `coolFunc()` has:\n// * 2 stubbings with 0 calls\n//   * Called 0 times: `(1, 2, 3) =\u003e 123`\n//   * Called 0 times: `(4, 5, 6) =\u003e { throw [Error: oh no] }`\n// * 1 unmatched call\n//   * `(1, 2, 4)`\n```\n\n#### `DebugOptions`\n\n```ts\nimport type { DebugOptions } from 'vitest-when'\n```\n\n| option | default | type    | description                            |\n| ------ | ------- | ------- | -------------------------------------- |\n| `log`  | `true`  | boolean | Whether the call to `debug` should log |\n\n#### `DebugResult`\n\n```ts\nimport type { DebugResult, Stubbing, Behavior } from 'vitest-when'\n```\n\n| fields                       | type                                         | description                                                 |\n| ---------------------------- | -------------------------------------------- | ----------------------------------------------------------- |\n| `description`                | `string`                                     | A human-readable description of the stub, logged by default |\n| `name`                       | `string`                                     | The name of the mock, if set by [`mockName`][mockName]      |\n| `stubbings`                  | `Stubbing[]`                                 | The list of configured stub behaviors                       |\n| `stubbings[].args`           | `unknown[]`                                  | The stubbing's arguments to match                           |\n| `stubbings[].behavior`       | `Behavior`                                   | The configured behavior of the stubbing                     |\n| `stubbings[].behavior.type`  | `return`, `throw`, `resolve`, `reject`, `do` | Result type of the stubbing                                 |\n| `stubbings[].behavior.value` | `unknown`                                    | Value for the behavior, if `type` is `return` or `resolve`  |\n| `stubbings[].behavior.error` | `unknown`                                    | Error for the behavior, it `type` is `throw` or `reject`    |\n| `stubbings[].matchedCalls`   | `unknown[][]`                                | Actual calls that matched the stubbing, if any              |\n| `unmatchedCalls`             | `unknown[][]`                                | Actual calls that did not match a stubbing                  |\n\n[mockName]: https://vitest.dev/api/mock.html#mockname\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmcous%2Fvitest-when","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmcous%2Fvitest-when","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmcous%2Fvitest-when/lists"}