Mocking
7. When do you use jest.fn, jest.spyOn, jest.mock and jest.replaceProperty?
Each one replaces a different thing:
jest.fn() creates a standalone mock function, for callbacks and injected dependencies.
jest.spyOn(obj, 'method') wraps an existing method, records calls and can be restored. Pass 'get' or 'set' as a third argument to spy on an accessor.
jest.mock('module') replaces a whole module in the registry, with an automock or a factory.
jest.replaceProperty(obj, key, value) swaps a non-function property, such as a config flag, and restores it later. The property must already exist.
The design point a senior candidate makes is that dependency injection beats module mocking where it is available.
A function that receives its HTTP client needs a jest.fn() and nothing else, and the test does not depend on Jest's module registry, hoisting or ESM support.
8. What is the difference between mockClear, mockReset and mockRestore?
Per the mock function API:
mockClear() empties mock.calls, mock.instances, mock.contexts and mock.results. The implementation stays.
mockReset() does that and replaces the implementation with an empty function that returns undefined.
mockRestore() does that and puts back the original implementation. It only works for mocks created with jest.spyOn().
The config options clearMocks, resetMocks and restoreMocks call the matching jest.*AllMocks() before every test, and all three default to false. jest.restoreAllMocks() only restores spies and replaced properties, so a module mock from a jest.mock factory keeps its jest.fn() implementations.
Teams usually turn on restoreMocks or clearMocks in config so call counts cannot leak from one test into the next (question 28).
9. Why does jest.mock run before the imports, and what limits does that put on the factory?
With babel-jest, babel-plugin-jest-hoist moves jest.mock calls above the imports in the compiled CommonJS, so the module is mocked before anything requires it.
Because the factory runs before the file's own variables are initialized, the plugin rejects factories that reference out-of-scope variables, with one escape hatch: names that start with mock.
import {getUser} from './api';
import {loadProfile} from './profile';
const mockGetUser = jest.fn(); // allowed: name starts with "mock"
jest.mock('./api', () => ({
...jest.requireActual('./api'), // keep the other exports real
getUser: (...args) => mockGetUser(...args),
}));
test('loads the profile', async () => {
mockGetUser.mockResolvedValue({id: 1, name: 'Ada'});
await expect(loadProfile(1)).resolves.toMatchObject({name: 'Ada'});
});
The arrow wrapper delays reading mockGetUser until the first call, by which time the variable exists.
A strong candidate also knows that Jest 30 made jest.mock paths case-sensitive, so jest.mock('./API') no longer matches api.js, and that hoisting does not apply to native ESM (question 19).
10. Why does a spy on a module export sometimes see zero calls?
Because the code under test does not call the export. A function that calls a sibling inside the same module uses the local binding, so replacing the property on the exports object changes nothing for that internal call.
// pricing.js
export function taxFor(amount) { return amount * 0.08; }
export function total(amount) { return amount + taxFor(amount); }
// pricing.test.js
import * as pricing from './pricing';
test('uses taxFor', () => {
const spy = jest.spyOn(pricing, 'taxFor');
pricing.total(100);
expect(spy).toHaveBeenCalled(); // fails: total() calls the local taxFor
});
Under native ESM the spy fails earlier, because a module namespace object is frozen and spyOn cannot redefine its properties.
Fixes, best first: test total by its output and leave taxFor alone; inject the tax function; or move taxFor to its own module and mock that module.
Rewriting production code to call exports.taxFor so a test can intercept it is the answer a reviewer should push back on.
11. How do you type mocks in TypeScript without casting?
Use jest.mocked() on the imported value, and the jest.Mocked, jest.MockedFunction and jest.Spied utility types for declarations. With @jest/globals imports, Jest 30 removed the old SpyInstance type in favor of jest.Spied.
import {jest, test, expect} from '@jest/globals';
import {fetchRates} from './rates';
import {convert} from './convert';
jest.mock('./rates');
test('converts with the fetched rate', async () => {
jest.mocked(fetchRates).mockResolvedValue({EUR: 0.9});
await expect(convert(100, 'EUR')).resolves.toBe(90);
});
Jest 30 also tightened the TypeScript types of the toHaveBeenCalledWith family, which is compile-time only but surfaces wrong argument shapes in existing tests during an upgrade.
A weak answer reaches for as jest.Mock everywhere, which turns off the checking these types exist for.
12. How does whenCalledWith change argument-dependent mocks?
Added in Jest 30.5.0, mockFn.whenCalledWith(...args) attaches a return value or implementation that fires only when the arguments match, including asymmetric matchers.
Before it, the same mock needed a mockImplementation with a switch on its arguments.
const getFlag = jest.fn().mockReturnValue(false);
getFlag.whenCalledWith('new-checkout').mockReturnValue(true);
getFlag.whenCalledWith(expect.stringMatching(/^beta-/)).mockReturnValue(true);
getFlag('new-checkout'); // true
getFlag('beta-search'); // true
getFlag('dark-mode'); // false, falls through to the base mock
The details from the docs that a candidate should know: arity is strict, so whenCalledWith('a') does not match fn('a', 'b'), and when a matched branch has no value left the call falls through to the base mock.
Vitest 5 shipped a similar vi.when API, which matters for teams keeping tests portable between the two (question 25).
13. What do the using keyword and jest.onGenerateMock add?
Jest 30 lets a spy be declared with using, so it is restored when the block ends even if an assertion throws. That removes a class of leaks where a spy on console or Date outlives its test because mockRestore sat after a failing line.
test('warns once', () => {
using warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
runMigration();
expect(warn).toHaveBeenCalledTimes(1);
}); // console.warn is restored here
jest.onGenerateMock(callback) runs for each generated automock and can change it before it is returned, for example to give every mocked database client a connect that resolves.
It suits large codebases that rely on automocking; most teams with factories will not need it.
The using syntax needs a TypeScript or Babel setup that compiles explicit resource management, which a candidate should check before recommending it.