Create a Spy Function (Test Double)
Arguments
impl: Function (optional) —Optional implementation to call. The spy must preserve the caller's this value.
Returns
Function — Solution idea
A spy is just a function with extra state. Record calls in arrays, and if an implementation is provided, delegate to it using apply() so this binding is preserved.
const spy = createSpy((a, b) => a + b);
spy(1, 2); // => 3
spy.calls; // => [[1, 2]]
spy.callCount(); // => 1
const obj = { x: 2, fn: createSpy(function (n) { return this.x + n; }) };
obj.fn(3); // => 5 (this preserved)
obj.fn.thisValues[0] === obj; // trueSolution
Overview
A spy is just a function with extra state. Record calls in arrays, and if an implementation is provided, delegate to it using apply() so this binding is preserved.
Approach: Closure + attached metadata
Steps:
1) Create a function spy(...args).
2) Push args into spy.calls and this into spy.thisValues.
3) If impl exists, return impl.apply(this, args).
4) Attach helpers: callCount(), lastCall(), reset().
Complexity: O(1) per call and O(k) memory for k calls.
export default function createSpy(impl) {
function spy(...args) {
spy.calls.push(args);
spy.thisValues.push(this);
if (typeof impl === 'function') {
return impl.apply(this, args);
}
return undefined;
}
spy.calls = [];
spy.thisValues = [];
spy.callCount = () => spy.calls.length;
spy.lastCall = () => (spy.calls.length ? spy.calls[spy.calls.length - 1] : null);
spy.reset = () => {
spy.calls.length = 0;
spy.thisValues.length = 0;
};
return spy;
}
export type SpyFn<TArgs extends any[] = any[], TResult = any> = ((...args: TArgs) => TResult) & {
calls: TArgs[];
thisValues: any[];
callCount: () => number;
lastCall: () => TArgs | null;
reset: () => void;
};
export default function createSpy<TArgs extends any[] = any[], TResult = any>(
impl?: (this: any, ...args: TArgs) => TResult
): SpyFn<TArgs, TResult> {
const spy = function (this: any, ...args: TArgs) {
(spy as any).calls.push(args);
(spy as any).thisValues.push(this);
if (typeof impl === 'function') {
return impl.apply(this, args);
}
return undefined as any as TResult;
} as SpyFn<TArgs, TResult>;
spy.calls = [] as TArgs[];
spy.thisValues = [] as any[];
spy.callCount = () => spy.calls.length;
spy.lastCall = () => (spy.calls.length ? spy.calls[spy.calls.length - 1] : null);
spy.reset = () => {
spy.calls.length = 0;
spy.thisValues.length = 0;
};
return spy;
}
Notes & Pitfalls
- Calling impl(...args) loses the caller's this; use impl.apply(this, args).
- Forgetting to reset between tests causes order-dependent failures.
- Asserting too much about call order can make tests brittle; prefer behavior-based assertions.
- No implementation: spy still records calls and returns undefined.
- Zero calls: lastCall() should return null.
- The spy may be used as a method; thisValues should reflect the receiver.
- Closure to keep state private.
- Attach metadata onto the function object.
- Preserve this via apply().
Common mistakes on this challenge
Spy metadata or delegation behavior is incorrect
Spy must record calls/this and preserve behavior when implementation exists.
- Push each call args into `spy.calls` and receiver into `spy.thisValues`.
- Delegate using `impl.apply(this, args)` to preserve method `this`.
- Expose `callCount`, `lastCall`, and `reset` with correct empty-state behavior.
Resources
Similar questions
Guides
Preparing for interviews? Use Frontend Coding Challenges first, then move into a concrete Study Plan before targeted Company Prep.