Jest support for mock argument-matched return values.
Jest support for mock argument-matched return values.
Train Jest mocks by argument list.
jest-when lets you keep Jest's familiar mock API while returning different values for different calls, without stuffing branching logic into mockImplementation.
Plain Jest makes one thing easy:
const fn = jest.fn()
fn.mockReturnValue('yay!')
But that returns 'yay!' no matter how fn is called.
If you want different behavior for different arguments, the usual alternative is a custom implementation with if statements inside your test. That works, but it gets noisy fast.
With jest-when:
import { when } from 'jest-when'
const fn = jest.fn()
when(fn).calledWith(1).mockReturnValue('yay!')
Now fn(1) returns 'yay!', and non-matching calls fall through to undefined unless you configure a default.
A good default mental model is:
calledWith(...) for the normal casemockReturnValue*, mockResolvedValue*, and mockRejectedValue* just like you already do in Jestdefault* methods when you want a fallbackexpectCalledWith(...) when unexpected calls should fail loudlywhen.allArgs(...) for advanced matching across the entire argument listnpm install --save-dev jest-when
>= 27import { when, resetAllWhenMocks, verifyAllWhenMocksCalled } from 'jest-when'
If you use CommonJS, require('jest-when') works too.
import { when } from 'jest-when'
const fn = jest.fn()
when(fn).calledWith(1).mockReturnValue('one')
when(fn).calledWith(2).mockReturnValue('two')
expect(fn(1)).toBe('one')
expect(fn(2)).toBe('two')
expect(fn(3)).toBeUndefined()
[!IMPORTANT]
calledWith(...)uses exact arity matching.
when(fn).calledWith(1)matchesfn(1). It does not matchfn(),fn(1, 2), orfn(1, undefined).
Async values work the same way:
const fetchUser = jest.fn()
when(fetchUser).calledWith(1).mockResolvedValue({ id: 1, role: 'admin' })
when(fetchUser).calledWith(2).mockRejectedValue(new Error('not found'))
await expect(fetchUser(1)).resolves.toEqual({ id: 1, role: 'admin' })
await expect(fetchUser(2)).rejects.toThrow('not found')
when(fn) wraps a Jest mock or spyStart with any normal Jest mock function or spy, then train it:
const fn = jest.fn()
when(fn)
.calledWith('hello')
.mockReturnValue('world')
when() also supports jest.spyOn(...):
const spy = jest.spyOn(api, 'fetchUser')
when(spy).calledWith(123).mockResolvedValue({ id: 123 })
calledWith(...) trains a specific callwhen(fn).calledWith(1, true, 'foo').mockReturnValue('yay!')
That training matches only that exact argument list.
when(fn)
.calledWith(1).mockReturnValue('yay!')
.calledWith(2).mockReturnValue('nay!')
expect(fn(1)).toBe('yay!')
expect(fn(2)).toBe('nay!')
Once trainings replace earlier ones for the same matcherswhen(fn).calledWith(1).mockReturnValue('old')
when(fn).calledWith(1).mockReturnValue('new')
expect(fn(1)).toBe('new')
*Once trainings are queued and removed after usewhen(fn)
.calledWith(1)
.mockReturnValueOnce('first')
.mockReturnValue('later')
expect(fn(1)).toBe('first')
expect(fn(1)).toBe('later')
expect(fn(1)).toBe('later')
The clearest way to add a fallback is with a default* method:
when(fn)
.calledWith('foo').mockReturnValue('special')
.defaultReturnValue('default')
expect(fn('foo')).toBe('special')
expect(fn('bar')).toBe('default')
You can place the default anywhere in the chain.
You can also set the fallback by calling a Jest-style method on the when(fn) chain before any calledWith(...):
when(fn)
.mockReturnValue('default')
.calledWith('foo').mockReturnValue('special')
That behaves the same as defaultReturnValue('default').
null, and friendswhen(fn).calledWith(1).mockReturnValue('number')
when(fn).calledWith({ role: 'admin' }).mockReturnValue('object')
when(fn).calledWith([1, 2, 3]).mockReturnValue('array')
when(fn).calledWith(/abc/).mockReturnValue('regex')
when(fn).calledWith(null).mockReturnValue('null')
Anything that works well with Jest's equality matching also works well here:
when(fn)
.calledWith(
expect.anything(),
expect.any(Number),
expect.objectContaining({ enabled: true })
)
.mockReturnValue('matched')
Wrap a regular predicate function with when(...) to use it as an argument matcher.
const allValuesTrue = when((arg: Record) => Object.values(arg).every(Boolean))
const divisibleBy3 = when((arg: number) => arg % 3 === 0)
when(fn)
.calledWith(allValuesTrue, divisibleBy3)
.mockReturnValue('yay!')
expect(fn({ a: true, b: true }, 9)).toBe('yay!')
expect(fn({ a: true, b: false }, 9)).toBeUndefined()
when.allArgs(...)Use when.allArgs(...) when matching one argument at a time is awkward and you want to evaluate the entire argument list at once.
const areNumberArgs = (args, equals) => args.every((arg) => equals(arg, expect.any(Number)));
when(fn).calledWith(when.allArgs(areNumberArgs)).mockReturnValue('all numbers')
expect(fn(3, 6, 9)).toBe('all numbers')
expect(fn(3, 666)).toBe('all numbers')
expect(fn(123, 'not a number')).toBeUndefined()
A handy partial-match pattern:
const firstArgMatches = (matcher: unknown) =>
when.allArgs((args, equals) => equals(args[0], matcher))
when(fn).calledWith(firstArgMatches(expect.any(Number))).mockReturnValue('yay!')
[!IMPORTANT]
when.allArgs(...)must be the only matcher passed tocalledWith(...)orexpectCalledWith(...).
| Export | What it does |
|---|---|
when(fn) |
Wraps a Jest mock or spy so you can train behavior by arguments. |
when(matcherFn) |
Turns a regular predicate into a function matcher. |
when.allArgs(fn) |
Creates a matcher that receives the entire argument list at once. |
resetAllWhenMocks() |
Removes all jest-when trainings and restores original mock implementations. |
verifyAllWhenMocksCalled() |
Asserts that every configured training was matched at least once. |
WhenMock |
Exported for advanced usage and typing. Most users should not need to import it directly. |
| default export | Available for compatibility. Named imports are the recommended API. |
| Method | Purpose |
|---|---|
calledWith(...matchers) |
Train behavior for an exact argument list. |
expectCalledWith(...matchers) |
Like calledWith, but throws an assertion error if the mock is called with different args. |
| Method | Purpose |
|---|---|
mockReturnValue(value) |
Return a value for matching calls. |
mockReturnValueOnce(value) |
Return a value once for matching calls. |
mockResolvedValue(value) |
Resolve a promise for matching calls. |
mockResolvedValueOnce(value) |
Resolve a promise once for matching calls. |
mockRejectedValue(error) |
Reject a promise for matching calls. |
mockRejectedValueOnce(error) |
Reject a promise once for matching calls. |
mockImplementation(fn) |
Use a custom implementation for matching calls. |
mockImplementationOnce(fn?) |
Use a custom implementation once for matching calls. |
| Method | Purpose |
|---|---|
defaultReturnValue(value) |
Fallback return value when no training matches. |
defaultResolvedValue(value) |
Fallback resolved promise when no training matches. |
defaultRejectedValue(error) |
Fallback rejected promise when no training matches. |
defaultImplementation(fn) |
Fallback implementation when no training matches. |
| Method | Purpose |
|---|---|
mockReset() |
Removes trainings for the current calledWith(...) / expectCalledWith(...) matcher set. |
resetWhenMocks() |
Removes all jest-when trainings for one mock or spy. |
resetAllWhenMocks() |
Removes all jest-when trainings across the entire test run. |
verifyAllWhenMocksCalled() |
Fails if any configured training was never matched. |
| Call | Effect |
|---|---|
fn.mockReset() |
Resets the underlying Jest mock and removes all jest-when trainings for that mock. |
when(fn).calledWith(1, 2, 3).mockReset() |
Removes only the training(s) for that exact matcher set. |
when(fn).resetWhenMocks() |
Removes all jest-when trainings for that one mock and restores its original implementation. |
resetAllWhenMocks() |
Removes all jest-when trainings across all wrapped mocks. |
expectCalledWith(...)expectCalledWith(...) is intentionally stricter than calledWith(...):
when(fn).expectCalledWith(1).mockReturnValue('x')
fn(2) // throws a helpful Jest assertion error
It is best when you want the mock itself to fail loudly on unexpected calls.
It is less pleasant with lots of compound declarations, because one unmatched branch will still fail the assertion.
v4 is rewritten in TypeScript and has much better inference, while keeping the same core API shape.
Most of the time you should not need to import any jest-when types at all.
const getUser = jest.fn(async (id: number) => ({ id, name: 'original' }))
when(getUser).calledWith(1).mockResolvedValue({ id: 1, name: 'Ada' })
await expect(getUser(1)).resolves.toEqual({ id: 1, name: 'Ada' })
That inference also works well with common patterns such as:
jest.fn(...)jest.spyOn(...)jest.mocked(...)jest-mock-extendedvoid, and async return typesIn other words: the docs can stay simple because the types should mostly just follow along.
A great default is one that throws:
when(fn)
.calledWith('expected').mockReturnValue('ok')
.defaultImplementation((...args) => {
throw new Error(`Unexpected args: ${JSON.stringify(args)}`)
})
const callback = jest.fn()
when(fn)
.calledWith(callback)
.mockImplementation((cb) => cb())
fn(callback)
expect(callback).toHaveBeenCalled()
when(fn).calledWith(1, 2, 3).mockReturnValue('yay!')
when(fn).calledWith(2).mockReturnValue('boo!')
when(fn).calledWith(1, 2, 3).mockReset()
expect(fn(1, 2, 3)).toBeUndefined()
expect(fn(2)).toBe('boo!')
import { verifyAllWhenMocksCalled, when } from 'jest-when'
const fn = jest.fn()
when(fn).calledWith(1).mockReturnValue('x')
fn(1)
verifyAllWhenMocksCalled()
This checks that every configured training was matched at least once.
Created by @timkindberg.
Many thanks to the people who helped shape and steward the project, especially:
No open issues yet, or sync has not completed.