jest-when: train a Jest mock by its argument list
Jest support for mock argument-matched return values.
At a glance
- What is it?
- A small TypeScript extension for Jest that lets you say what a mock returns for which arguments, instead of burying that logic in a mockImplementation callback. It keeps the Jest API you already use and adds literal, asymmetric, predicate and whole-argument-list matchers on top.
- Who is it for?
- Adopt jest-when if your tests have mocks whose behaviour depends on the arguments, since the branching moves out of the test body and the matcher set covers the cases you would otherwise hand roll. Skip it if a single mockReturnValue already covers your case, since the extra dependency is not worth it.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 180 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The problem is one mock with many possible inputs
Jest makes one thing easy:
const fn = jest.fn()
fn.mockReturnValue('yay!')But that returns 'yay!' no matter how fn is called. The moment a function is called with different arguments and should behave differently, the usual alternative is a custom implementation with if statements inside your test, which works but gets noisy fast.
jest-when keeps the familiar mock API and moves the branching into declarations. The library describes itself as training Jest mocks by argument list:
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.
That fall-through behaviour is the part worth internalising. A plain Jest mock with mockReturnValue returns the same thing for every call, so a test that forgets to set up an argument combination still gets a plausible value and the bug hides. With jest-when, an untrained argument list produces undefined, which is usually a loud failure in an assertion rather than a quiet wrong answer.
The audience is anyone writing Jest tests in JavaScript or TypeScript who has written a mockImplementation with a switch statement in it and wished they did not have to.
Install and compatibility, including the older Jest workspace
Installation is a single development dependency:
npm install --save-dev jest-whenThe compatibility floor is Jest 27 or later, and the package declares that as a peerDependency rather than a hard dependency, which is the right call for a test helper because it means the library never installs a second copy of Jest into your tree.
Named imports are recommended, and the import line the documentation gives is:
import { when, resetAllWhenMocks, verifyAllWhenMocksCalled } from 'jest-when'If you use CommonJS, require('jest-when') works too.
The repository shows how seriously the compatibility floor is taken. The package manifest has a workspaces entry pointing at compat/jest27, and there is a dedicated script, verify:jest27, that runs the compat workspace's own verify. The full verify script chains lint, typecheck, build, the test suite with --runInBand and the Jest 27 compatibility check, and prepublishOnly runs that verify, so a release cannot go out with the older Jest line broken.
The published artefact is small and deliberate. The main entry is dist/when.js and the types entry is dist/when.d.ts, and the files list contains only dist, README.md and LICENSE, so nothing from the test infrastructure or the compat workspace ends up in the package you install.
Exact arity matching, and why your fallback plan depends on it
This is the rule that catches people out, and the documentation puts it in an IMPORTANT callout rather than a footnote.
calledWith(...) uses exact arity matching. when(fn).calledWith(1) matches fn(1). It does not match fn(), it does not match fn(1, 2), and it does not match fn(1, undefined).
That last case is the subtle one. In JavaScript, fn(1) and fn(1, undefined) are different calls as far as arguments.length is concerned, and jest-when takes that literally. So a test that calls a function with an explicit undefined argument will not hit a training written for one argument.
The consequence for how you write tests is that you cannot rely on a catch-all training to absorb stray arguments. If your production code has a path that calls the function with a different number of arguments, you get undefined rather than a default, and you need either an explicit training for that arity or an explicit default.
The README gives a mental model for choosing the right tool for each situation. Use calledWith for the normal case. Use Jest asymmetric matchers when literals are too specific. Use the mockReturnValue, mockResolvedValue and mockRejectedValue family just like you already do in Jest. Use the default methods when you want a fallback. Use expectCalledWith when unexpected calls should fail loudly. Use when.allArgs for advanced matching across the entire argument list.
Matchers beyond literals: asymmetric, predicate and allArgs
The matcher set is what makes the library usable on real code rather than on toy signatures.
Literals work as you would expect, and objects, arrays, regexes and null are all supported:
when(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')Jest asymmetric matchers work too, since anything that works well with Jest's equality matching works here:
when(fn)
.calledWith(
expect.anything(),
expect.any(Number),
expect.objectContaining({ enabled: true })
)
.mockReturnValue('matched')Function matchers are the distinctive addition. You wrap a regular predicate function with when(...) to use it as an argument matcher:
const allValuesTrue = when((arg: Record<string, boolean>) => Object.values(arg).every(Boolean))
const divisibleBy3 = when((arg: number) => arg % 3 === 0)The predicate receives one argument at a time, which is why two separate wrappers are needed for two separate positions.
when.allArgs is the escape hatch for when matching one argument at a time is awkward. It receives the entire argument list at once, and the matcher function is given the args array and Jest's equals function:
const areNumberArgs = (args, equals) => args.every((arg) => equals(arg, expect.any(Number)))
when(fn).calledWith(when.allArgs(areNumberArgs)).mockReturnValue('all numbers')There is one hard constraint on it: when.allArgs(...) must be the only matcher passed to calledWith(...) or expectCalledWith(...). And the README shows a handy partial-match pattern built on it, a firstArgMatches helper that compares only args[0] against a matcher you supply.
Chaining, once variants, and the queueing rule
The chaining rules are where a lot of the subtlety lives, and they are documented precisely.
Trainings can be chained on one when(fn) call, each with its own argument list:
when(fn)
.calledWith(1).mockReturnValue('yay!')
.calledWith(2).mockReturnValue('nay!')Later non-Once trainings replace earlier ones for the same matchers. So training calledWith(1) twice leaves the second value in place, and the last write wins for that argument list.
The Once variants behave the way Jest's own Once methods do, with one addition: they are queued and removed after use. The example chains a once value followed by a persistent one:
when(fn)
.calledWith(1)
.mockReturnValueOnce('first')
.mockReturnValue('later')The first call returns 'first', and the second and third both return 'later'. So a Once training is consumed once and then falls through to the persistent training for the same arguments, rather than to undefined. That is what makes a sequence of responses expressible without writing a counter in the test.
It also means that ordering matters when you mix once and persistent trainings on the same arguments, since a later persistent training would replace an earlier one for the same matchers, while the Once variant stays queued until it is used.
Defaults are explicit, and the fallback belongs to you
A fallback value is not implicit in this library. It has to be stated, which is a deliberate design choice.
The clearest way to add one is the defaultReturnValue method, and you can place the default anywhere in the chain:
when(fn)
.calledWith('foo').mockReturnValue('special')
.defaultReturnValue('default')So fn('foo') returns 'special' and fn('bar') returns 'default'.
There is a second form. You can call a Jest-style method on the when(fn) chain before any calledWith, and it behaves the same as defaultReturnValue:
when(fn)
.mockReturnValue('default')
.calledWith('foo').mockReturnValue('special')The documentation frames this as defaults being explicit in v3 and still familiar in v4, which reflects the fact that earlier versions of the library made the default behaviour implicit and later versions required you to say so.
This matters in practice because the two designs have opposite failure modes. An implicit default means a test never sees undefined and an argument list you forgot to train is silently handled by the catch-all. An explicit default means you have written down what should happen for every other input, which is a small amount of extra code and a considerably better test. The library also keeps the loud failure option, since expectCalledWith is there for the case where unexpected calls should fail rather than be absorbed by a default.
Async values and the per-test teardown you must not forget
Promises are trained with the same matchers as synchronous values, using the resolved and rejected variants:
const fetchUser = jest.fn()
when(fetchUser).calledWith(1).mockResolvedValue({ id: 1, role: 'admin' })
when(fetchUser).calledWith(2).mockRejectedValue(new Error('not found'))So the whole point of the library is that the argument-to-behaviour mapping is written once, and every test that exercises the function reads the same way.
The one thing the documentation asks you to handle is cleanup. The package exports resetAllWhenMocks, which removes all jest-when trainings, and verifyAllWhenMocksCalled, for the case where you want to assert that the calls you trained actually happened. Both are imported from the same named import line.
This is a real operational requirement rather than a formality. A jest-when training is state attached to the mock function, and jest.fn() creates a fresh mock per test, so the usual Jest reset in your setup file handles most cases. But if a mock is created once at module scope and reused across tests, or if you share a spy, the trainings accumulate. resetAllWhenMocks exists for exactly that situation and belongs in your afterEach.
One more detail worth knowing: when(fn) wraps a Jest mock function or a spy, and the documentation shows that jest.spyOn(...) is supported, so the same training style works for the collaborator methods you are stubbing rather than only for functions you define inline.
Where this library is the wrong tool
There are a few situations where it does not earn its place.
The first is when a single return value is enough. If your mock always returns the same thing regardless of arguments, plain Jest already does that with mockReturnValue, and adding a dependency for it is pure overhead. The library's value is entirely in the argument dimension.
The second is when the branching is genuinely about state rather than arguments. jest-when trains on what the function was called with. If your mock needs to respond to a sequence of calls, or to an external condition, you are back to mockImplementation, and the library will not remove that code.
The third is the arity rule interacting badly with your production code. If the function under test is called with a variable argument list, or called with explicit undefined values, exact arity matching means you need more trainings than you expected, and a catch-all default is not a substitute for a training that matches.
The fourth is a constraint that applies to every Jest extension: it only works in Jest. The peerDependency is jest at 27 or later, so this is not usable in Vitest or any other runner.
And the fifth is the state of the project. The repository is not archived and the last push was on 2026-04-26, with v4.0.3 published the same day, and the version before that, v4.0.0 on 2026-03-23. Before v4 there was v3.8.0-0, whose release note says Convert to TypeScript. So the library moved to TypeScript and then went through a major version, which means the v3 documentation you may find in an older tutorial is not the v4 documentation, particularly around defaults, which the README describes as explicit from v3 onwards but still changing shape in v4.
Editorial conclusion
Adopt jest-when if your tests have mocks whose behaviour depends on the arguments, since the branching moves out of the test body and the matcher set covers the cases you would otherwise hand roll. Skip it if a single mockReturnValue already covers your case, since the extra dependency is not worth it. Verify first that your Jest version is 27 or later, that you understand the exact arity matching rule before relying on a fallback for wrong arguments, and that your teardown calls resetAllWhenMocks so trainings do not leak between tests.
Frequently asked questions
How do I install jest-when and which Jest versions does it support?
Install it with npm install --save-dev jest-when. Jest 27 and later is supported, and the package declares jest as a peer dependency rather than bundling its own copy. It works in both JavaScript and TypeScript projects.
Does when(fn).calledWith(1) match a call with extra arguments?
No. calledWith(...) uses exact arity matching, so when(fn).calledWith(1) matches fn(1) but not fn(), fn(1, 2) or fn(1, undefined). Non-matching calls fall through to undefined unless you configure a default.
How do I set a fallback value for calls jest-when does not match?
Use the defaultReturnValue method, which can be placed anywhere in the chain, or call a Jest-style method on the when(fn) chain before any calledWith, which behaves the same as defaultReturnValue.
Can I match arguments with a custom predicate in jest-when?
Yes. Wrap a regular predicate function with when(...) to use it as an argument matcher, and use when.allArgs(...) to evaluate the entire argument list at once, which receives the args array and Jest's equals function. when.allArgs must be the only matcher passed to calledWith.
How do I clean up jest-when trainings between tests?
The package exports resetAllWhenMocks, which removes all jest-when trainings, and verifyAllWhenMocksCalled for asserting the trained calls happened. Both are named imports from jest-when, alongside when.
How do I train a mock to resolve or reject in jest-when?
Use mockResolvedValue and mockRejectedValue with the same calledWith matchers as the synchronous forms, so when(fetchUser).calledWith(2).mockRejectedValue(new Error('not found')) makes that call return a rejected promise.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/timkindberg-jest-when)Was this useful?