npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

vitest-when

v0.5.0

Published

Stub behaviors of Vitest mock functions with a small, readable API.

Downloads

48,921

Readme

vitest-when

npm badge ci badge coverage badge

Read the introductory post: Better mocks in Vitest

Stub behaviors of Vitest mock functions with a small, readable API. Inspired by testdouble.js and jest-when.

npm install --save-dev vitest-when

Usage

Create stubs - fake objects that have pre-configured responses to matching arguments - from Vitest's mock functions. With vitest-when, your stubs are:

  • Easy to read
  • Hard to misconfigure, especially when using TypeScript

Wrap your vi.fn() mock - or a function imported from a vi.mock'd module - in when, match on a set of arguments using calledWith, and configure a behavior

If 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.

import { vi, test, afterEach } from 'vitest'
import { when } from 'vitest-when'

afterEach(() => {
  vi.resetAllMocks()
})

test('stubbing with vitest-when', () => {
  const stub = vi.fn()

  when(stub).calledWith(1, 2, 3).thenReturn(4)
  when(stub).calledWith(4, 5, 6).thenReturn(7)

  let result = stub(1, 2, 3)
  expect(result).toBe(4)

  result = stub(4, 5, 6)
  expect(result).toBe(7)

  result = stub(7, 8, 9)
  expect(result).toBe(undefined)
})

You should call vi.resetAllMocks() in your suite's afterEach hook to remove the implementation added by when. You can also set Vitest's mockReset config to true instead of using afterEach.

Why not vanilla Vitest mocks?

Vitest'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.

  • Mock usage is spread across the arrange and assert phases of your test, with "act" in between, making the test harder to read.
  • If you forget the expect(...).toHaveBeenCalledWith(...) step, the test will pass even if the mock is called incorrectly.
  • expect(...).toHaveBeenCalledWith(...) is not type-checked, as of Vitest 0.31.0.
// arrange
const stub = vi.fn()
stub.mockReturnValue('world')

// act
const result = stub('hello')

// assert
expect(stub).toHaveBeenCalledWith('hello')
expect(result).toBe('world')

In contrast, when using vitest-when stubs:

  • All stub configuration happens in the "arrange" phase of your test.
  • You cannot forget calledWith.
  • calledWith and thenReturn (et. al.) are fully type-checked.
// arrange
const stub = vi.fn()
when(stub).calledWith('hello').thenReturn('world')

// act
const result = stub('hello')

// assert
expect(result).toBe('world')

Example

See the ./example directory for example usage.

// meaning-of-life.test.ts
import { vi, describe, afterEach, it, expect } from 'vitest'
import { when } from 'vitest-when'

import * as deepThought from './deep-thought.ts'
import * as earth from './earth.ts'
import * as subject from './meaning-of-life.ts'

vi.mock('./deep-thought.ts')
vi.mock('./earth.ts')

describe('get the meaning of life', () => {
  afterEach(() => {
    vi.resetAllMocks()
  })

  it('should get the answer and the question', async () => {
    when(deepThought.calculateAnswer).calledWith().thenResolve(42)
    when(earth.calculateQuestion).calledWith(42).thenResolve("What's 6 by 9?")

    const result = await subject.createMeaning()

    expect(result).toEqual({ question: "What's 6 by 9?", answer: 42 })
  })
})
// meaning-of-life.ts
import { calculateAnswer } from './deep-thought.ts'
import { calculateQuestion } from './earth.ts'

export interface Meaning {
  question: string
  answer: number
}

export const createMeaning = async (): Promise<Meaning> => {
  const answer = await calculateAnswer()
  const question = await calculateQuestion(answer)

  return { question, answer }
}
// deep-thought.ts
export const calculateAnswer = async (): Promise<number> => {
  throw new Error(`calculateAnswer() not implemented`)
}
// earth.ts
export const calculateQuestion = async (answer: number): Promise<string> => {
  throw new Error(`calculateQuestion(${answer}) not implemented`)
}

API

when(spy: TFunc, options?: WhenOptions): StubWrapper<TFunc>

Configures 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(...)

import { vi } from 'vitest'
import { when } from 'vitest-when'

const spy = vi.fn()

when(spy)

expect(spy()).toBe(undefined)

Options

import type { WhenOptions } from 'vitest-when'

| option | default | type | description | | ------- | ------- | ------- | -------------------------------------------------- | | times | N/A | integer | Only trigger configured behavior a number of times |

.calledWith(...args: TArgs): Stub<TArgs, TReturn>

Create a stub that matches a given set of arguments which you can configure with different behaviors using methods like .thenReturn(...).

const spy = vi.fn()

when(spy).calledWith('hello').thenReturn('world')

expect(spy('hello')).toEqual('world')

When 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:

const spy = vi.fn()

when(spy).calledWith(expect.any(String)).thenReturn('world')

expect(spy('hello')).toEqual('world')
expect(spy('anything')).toEqual('world')

If calledWith is used multiple times, the last configured stubbing will be used.

when(spy).calledWith('hello').thenReturn('world')
expect(spy('hello')).toEqual('world')
when(spy).calledWith('hello').thenReturn('goodbye')
expect(spy('hello')).toEqual('goodbye')

Types of overloaded functions

Due 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:

function overloaded(): null
function overloaded(input: number): string
function overloaded(input?: number): string | null {
  // ...
}

// Last entry: all good!
when(overloaded).calledWith(42).thenReturn('hello')

// $ts-expect-error: first entry
when(overloaded).calledWith().thenReturn(null)

// Manually specified: all good!
when<() => null>(overloaded).calledWith().thenReturn(null)

Fallback

By 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.

const spy = vi.fn().mockReturnValue('you messed up!')

when(spy).calledWith('hello').thenReturn('world')

spy('hello') // "world"
spy('jello') // "you messed up!"

.thenReturn(value: TReturn)

When the stubbing is satisfied, return value

const spy = vi.fn()

when(spy).calledWith('hello').thenReturn('world')

expect(spy('hello')).toEqual('world')

To only return a value once, use the times option.

import { when } from 'vitest-when'

const spy = vi.fn()

when(spy, { times: 1 }).calledWith('hello').thenReturn('world')

expect(spy('hello')).toEqual('world')
expect(spy('hello')).toEqual(undefined)

You 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.

const spy = vi.fn()

when(spy).calledWith('hello').thenReturn('hi', 'sup?')

expect(spy('hello')).toEqual('hi')
expect(spy('hello')).toEqual('sup?')
expect(spy('hello')).toEqual('sup?')

.thenResolve(value: TReturn)

When the stubbing is satisfied, resolve a Promise with value

const spy = vi.fn()

when(spy).calledWith('hello').thenResolve('world')

expect(await spy('hello')).toEqual('world')

To only resolve a value once, use the times option.

import { when } from 'vitest-when'

const spy = vi.fn()

when(spy, { times: 1 }).calledWith('hello').thenResolve('world')

expect(await spy('hello')).toEqual('world')
expect(spy('hello')).toEqual(undefined)

You 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.

const spy = vi.fn()

when(spy).calledWith('hello').thenResolve('hi', 'sup?')

expect(await spy('hello')).toEqual('hi')
expect(await spy('hello')).toEqual('sup?')
expect(await spy('hello')).toEqual('sup?')

.thenThrow(error: unknown)

When the stubbing is satisfied, throw error.

const spy = vi.fn()

when(spy).calledWith('hello').thenThrow(new Error('oh no'))

expect(() => spy('hello')).toThrow('oh no')

To only throw an error only once, use the times option.

import { when } from 'vitest-when'

const spy = vi.fn()

when(spy, { times: 1 }).calledWith('hello').thenThrow(new Error('oh no'))

expect(() => spy('hello')).toThrow('oh no')
expect(spy('hello')).toEqual(undefined)

You 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.

const spy = vi.fn()

when(spy)
  .calledWith('hello')
  .thenThrow(new Error('oh no'), new Error('this is bad'))

expect(() => spy('hello')).toThrow('oh no')
expect(() => spy('hello')).toThrow('this is bad')
expect(() => spy('hello')).toThrow('this is bad')

.thenReject(error: unknown)

When the stubbing is satisfied, reject a Promise with error.

const spy = vi.fn()

when(spy).calledWith('hello').thenReject(new Error('oh no'))

await expect(spy('hello')).rejects.toThrow('oh no')

To only throw an error only once, use the times option.

import { times, when } from 'vitest-when'

const spy = vi.fn()

when(spy, { times: 1 }).calledWith('hello').thenReject(new Error('oh no'))

await expect(spy('hello')).rejects.toThrow('oh no')
expect(spy('hello')).toEqual(undefined)

You 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.

const spy = vi.fn()

when(spy)
  .calledWith('hello')
  .thenReject(new Error('oh no'), new Error('this is bad'))

await expect(spy('hello')).rejects.toThrow('oh no')
await expect(spy('hello')).rejects.toThrow('this is bad')
await expect(spy('hello')).rejects.toThrow('this is bad')

.thenDo(callback: (...args: TArgs) => TReturn)

When 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.

const spy = vi.fn()
let called = false

when(spy)
  .calledWith('hello')
  .thenDo(() => {
    called = true
    return 'world'
  })

expect(spy('hello')).toEqual('world')
expect(called).toEqual(true)

To only run the callback once, use the times option.

import { times, when } from 'vitest-when'

const spy = vi.fn()

when(spy, { times: 1 })
  .calledWith('hello')
  .thenDo(() => 'world')

expect(spy('hello')).toEqual('world')
expect(spy('hello')).toEqual(undefined)

You 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.

const spy = vi.fn()

when(spy)
  .calledWith('hello')
  .thenDo(
    () => 'world',
    () => 'solar system',
  )

expect(spy('hello')).toEqual('world')
expect(spy('hello')).toEqual('solar system')

debug(spy: TFunc, options?: DebugOptions): DebugResult

Logs 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.

import { when, debug } from 'vitest-when'

const coolFunc = vi.fn().mockName('coolFunc')

when(coolFunc).calledWith(1, 2, 3).thenReturn(123)
when(coolFunc).calledWith(4, 5, 6).thenThrow(new Error('oh no'))

const result = coolFunc(1, 2, 4)

debug(coolFunc)
// `coolFunc()` has:
// * 2 stubbings with 0 calls
//   * Called 0 times: `(1, 2, 3) => 123`
//   * Called 0 times: `(4, 5, 6) => { throw [Error: oh no] }`
// * 1 unmatched call
//   * `(1, 2, 4)`

DebugOptions

import type { DebugOptions } from 'vitest-when'

| option | default | type | description | | ------ | ------- | ------- | -------------------------------------- | | log | true | boolean | Whether the call to debug should log |

DebugResult

import type { DebugResult, Stubbing, Behavior } from 'vitest-when'

| fields | type | description | | ---------------------------- | -------------------------------------------- | ----------------------------------------------------------- | | description | string | A human-readable description of the stub, logged by default | | name | string | The name of the mock, if set by mockName | | stubbings | Stubbing[] | The list of configured stub behaviors | | stubbings[].args | unknown[] | The stubbing's arguments to match | | stubbings[].behavior | Behavior | The configured behavior of the stubbing | | stubbings[].behavior.type | return, throw, resolve, reject, do | Result type of the stubbing | | stubbings[].behavior.value | unknown | Value for the behavior, if type is return or resolve | | stubbings[].behavior.error | unknown | Error for the behavior, it type is throw or reject | | stubbings[].matchedCalls | unknown[][] | Actual calls that matched the stubbing, if any | | unmatchedCalls | unknown[][] | Actual calls that did not match a stubbing |