cswork / libraryOpen practice workspace

Articles

Jest Interview Questions: ESM Mocking, Fake Timers, and Async False Positives

Practice Jest interview questions with ESM mock-loading order, a 300 ms fake-timer trace, and awaited async assertions that prevent false positives.

A Jest interview can look easy until a test passes for the wrong reason. An ESM module was imported before the mock existed; a fake clock advanced while a Promise callback was still pending; an asynchronous assertion ran after the test had already finished. A good answer identifies which boundary was crossed and shows a test that would fail if the behavior regressed.

This guide uses original examples, not a claim that a particular employer asks these questions. Jest's ECMAScript modules guide, timer-mocks guide, and asynchronous testing guide are the authority for the API behavior. The order of module evaluation, timer advancement, and assertion execution is the practical skill to rehearse.

To put that sequence into a concrete UI exercise, use Build an Autocomplete Search Bar With a Hand-Written Debounce and predict the call count before advancing the test clock.

Interview question: why did my ESM mock not replace the import?

Start with the module-loading sequence. In ESM, a static import is evaluated before the rest of the importing module's body runs. Calling jest.mock below that import does not rewind the already evaluated dependency. Jest's ESM documentation describes this difference from the familiar CommonJS hoisting pattern and provides jest.unstable_mockModule for ESM mocking. The “unstable” name is part of the documented API; check the Jest version and runner configuration in the actual project before promising identical behavior everywhere.

Consider price.js, which imports fetchRate from rate-client.js. A test wants a deterministic rate of 1.2. Register the ESM mock first, then dynamically import the module under test:

import { jest, test, expect } from '@jest/globals';

jest.unstable_mockModule('./rate-client.js', () => ({
  fetchRate: jest.fn(async () => 1.2)
}));

const { totalInTargetCurrency } = await import('./price.js');

test('uses the returned rate', async () => {
  await expect(totalInTargetCurrency(10)).resolves.toBe(12);
});

The important claim is the sequence: registration, dynamic import, invocation, awaited assertion. If price.js was statically imported earlier by this test or setup code, its dependency could already be cached and the mock could miss the import path that mattered. If the module uses a default export, the factory must match that export shape. If the target is CommonJS, its mocking rules differ; establish the module format rather than blindly replacing every jest.mock call.

An interviewer may ask whether jest.spyOn is simpler. It can be when you have a mutable object method to observe after import, but it does not solve a dependency captured before a test's replacement. Explain what is imported, when it is evaluated, whether the export is mutable in your setup, and what the test needs to control. The strongest answer starts with the load graph, not a memorized mocking incantation.

A verification test should prove the mock was actually used. Assert the returned total and the fake client's call arguments; also try a rate that would make the real dependency's result visibly different. A passing arithmetic assertion alone might accidentally be consistent with both paths. If you need module isolation between tests, use Jest's documented module reset or isolation APIs with care, and avoid mutable singleton state that leaks through test order.

Interview question: what changes when Jest uses fake timers?

Fake timers replace selected time-related APIs with a controllable clock. They do not automatically settle every Promise or network request. Jest's timer-mocks guide explains jest.useFakeTimers(), clock advancement, pending timer execution, and restoration with jest.useRealTimers(). A test should control the timer and clean up even if another test follows.

Take an autocomplete handler that waits 300 milliseconds after the last keystroke before calling search. The behavior contract is that three rapid inputs produce one call with the latest query. The original trace is:

EventFake-clock timeExpected search calls
Input c0 ms0
Input ca100 ms0
Input cat200 ms0
Advance to 499 ms499 ms0
Advance to 500 ms500 ms1, with cat

The deadline is 300 ms after the last input, not 300 ms after the first. The support image records the same boundary. A test that jumps directly to 500 ms without asserting the earlier zero-call state misses a premature-call bug.

For a plain callback timer, advancing time and checking the mock may suffice. When the timer callback awaits a Promise, prefer the asynchronous timer advancement API, such as await jest.advanceTimersByTimeAsync(300), if supported by the project's Jest version and timer mode. Jest documents timer methods on the Jest object; the async variant gives queued Promise callbacks a chance to run between timer steps. Otherwise, explicitly await the microtask or returned work that the application exposes. Moving the clock and awaiting application completion are different operations.

Avoid runAllTimers for an indefinitely recurring timer. Jest has a guard against endless timer recursion, but triggering that guard is not a useful assertion. For an interval or recursive timeout, step only the intended number of cycles with runOnlyPendingTimers or a bounded advancement, and assert the stop condition. Do not assert only the final call count if the requirement is to stop on cleanup; assert that an additional clock advance after cleanup produces no more calls.

A reliable teardown restores the real clock. Depending on the suite, flush intentionally pending timers before restoration when callbacks matter, or cancel them through the code under test. Explain the chosen cleanup order: silently abandoning a scheduled callback can conceal a leak, while running every pending callback can create side effects in another test. Timer state is global to the Jest environment, so a leftover fake clock can cause an unrelated test to hang.

Why can an async test pass without checking anything?

Jest must know when asynchronous work finishes. The asynchronous testing guide says to return or await a Promise, or use the completion callback form correctly. A test that calls an async function and never returns its Promise can complete before an assertion runs. This is the classic false positive: the green check marks the test body's synchronous finish, not the behavior you intended to check.

This test is broken:

test('rejects an invalid token', () => {
  verifyToken('expired').then(() => {
    throw new Error('Expected rejection');
  });
});

Jest sees no returned or awaited Promise. Even if the callback later throws, its relationship to the reported test can be unreliable. Express the negative path as one awaited assertion:

test('rejects an invalid token', async () => {
  await expect(verifyToken('expired')).rejects.toThrow('expired');
});

If you use a try/catch shape, add an assertion count or an explicit failure when no rejection occurs. Jest's async guide shows expect.assertions(1) for that purpose. Otherwise, a fulfilled Promise may skip the catch block and leave a green test with zero assertions. This is not a reason to add expect.assertions to every test; use it where a branch must execute.

A second trap is awaiting the wrong thing. await startWork() may only enqueue a callback while the observable behavior appears later on a timer or event. Identify the application-level completion signal: a returned Promise, an emitted event, a rendered state transition, or a callback. Then wait for that signal. A sleep of an arbitrary number of milliseconds often makes a test slower without proving it is synchronized.

A third trap is combining a callback-style done argument with a returned Promise. Two completion channels can obscure who owns the test's end. Pick one. For modern Promise APIs, async/await usually gives the clearest failure path. For callback-only APIs, call done on success and pass an error to done on failure according to Jest's documented callback pattern. Explicitly assert that the callback fires once if duplicates are possible.

Interview question: how would you test a debounced async search?

Combine the boundaries instead of testing each tool in isolation. Suppose typing calls scheduleSearch(query). It waits 300 ms, calls an async searchApi, and applies results only for the latest query. Write the contract before choosing mocks: no request before the deadline; exactly one request for the latest query; a slow older response never replaces newer results; teardown cancels a pending timer.

Use one test for the clock boundary and another for the stale-response policy. In the clock test, install fake timers in a scoped setup, type the sequence, advance to just before the deadline, assert zero API calls, advance one more millisecond, await any Promise work, and assert one call with cat. In the stale-response test, use controllable deferred Promises. Resolve the newer response before the older one, then verify the displayed result stays new. That test may need two requests, so do not reuse a debounce-only trace that intentionally collapses them.

If the implementation creates an AbortController, asserting that the earlier signal is aborted shows client cancellation intent. It does not prove the server never processed the request. If the application prevents stale UI commits with a generation counter, test the visible state rather than assuming transport cancellation. The distinction matters for write requests, where a cancelled client subscription may not undo a server mutation.

For cleanup, unmount or dispose the component before the timer deadline, advance the clock past it, and assert no API call or state update. If the API request already started, dispose and settle it, then assert no update is applied after disposal. These are separate phases: cancelling a pending timer and ignoring a completed late response.

A concise debugging sequence follows the same ordering. First check whether the mock replaced the intended ESM import. Next inspect timer mode and pending timer count. Then inspect whether the test awaits the Promise or UI condition. Finally check teardown and singleton state. Changing all four at once can turn a flaky test green without explaining why.

What should you say in a code review or interview?

Offer a falsifiable prediction. “If I move mock registration after the static import, the real client may already be bound. If I advance only the fake clock, a queued Promise can still be pending. If I forget to await the assertion, Jest may finish before it runs.” Each prediction maps to a small test or instrumentation point.

QuestionJest practice angleFollow-up to answer aloud
Build an Autocomplete Search Bar With a Hand-Written DebounceFake-clock deadlineWhich assertion catches an early request?
Implement retry wrapper and interdependent validatorsAsync retriesWhat event tells Jest the final attempt ended?
Write good tests and define integration testsTest boundaryWhich dependency should remain real?
Validate Unit-Test Coverage and Identify Missing ScenariosFalse positivesWhich branch can go green without an assertion?
Debug React Sorting, Persistence, Routing, and AccessibilityUI async behaviorWhich user-visible condition should replace a fixed sleep?

One useful spoken answer is: “I draw the timeline first. With ESM, I register the mock before a dynamic import of the subject. With fake timers, I test the boundary immediately before and after the deadline, and await Promise work separately. With asynchronous assertions, I return or await the completion path, including negative branches. Then I restore timers and module state so test order cannot change the result.” The interviewer can change the module format, scheduling API, or UI contract; your answer should change with those facts.

For hands-on rehearsal, start with Build an Autocomplete Search Bar With a Hand-Written Debounce. Write a test that fails if the search fires at 499 ms, at 500 ms with the wrong query, or after cleanup. Each failure should identify a different boundary.

Sources and further reading