Hone logo
Hone
Problems

Jest Mocking: Creating a Spy Function

This challenge focuses on a fundamental aspect of unit testing: mocking and spying. You will learn how to create a "spy" function in Jest that allows you to track if a function was called, how many times it was called, and with what arguments, without altering the original function's behavior. This is crucial for verifying interactions between different parts of your code.

Problem Description

Your task is to implement a Jest "spy" function. A spy is a type of mock function that records information about its invocations. Specifically, you need to create a function that, when given an existing function, returns a new function (the spy). This spy function should:

  1. Delegate to the original function: When the spy is called, it should execute the original function with the same this context and arguments.
  2. Record invocation details: The spy must keep track of:
    • Whether it has been called at all.
    • The number of times it has been called.
    • The arguments passed to each call.
    • The this context for each call.
    • The return value of each call.
  3. Provide inspection methods: The spy should expose methods to inspect these recorded details. These methods should mimic common Jest spy functionalities.

Key Requirements:

  • Create a function named createSpy that accepts one argument: originalFn (the function to spy on).
  • createSpy should return a new function (the spy).
  • The returned spy function should behave like originalFn when called.
  • The spy must record:
    • called: A boolean indicating if the spy has been called.
    • callCount: An integer representing the number of times the spy has been called.
    • calls: An array of arrays, where each inner array contains the arguments of a single invocation.
    • thisContexts: An array storing the this context of each invocation.
    • results: An array of objects, where each object represents the outcome of a call. Each result object should have a type property ('return' or 'throw') and a value property containing the returned value or the thrown error.
  • The spy should have methods for inspection:
    • reset(): Clears all recorded call information.
    • getCall(index): Returns the arguments of the call at the specified index.
    • getThisContext(index): Returns the this context of the call at the specified index.
    • getReturnCount(): Returns the number of calls that returned a value (not thrown an error).
    • getThrowCount(): Returns the number of calls that threw an error.

Expected Behavior:

When you call the spy, the originalFn should execute, and the spy's internal state should update. You should then be able to use the inspection methods to query this state.

Edge Cases:

  • Handling functions that throw errors.
  • Handling functions with no arguments.
  • Handling functions that return undefined.
  • Ensuring this context is correctly preserved.

Examples

Example 1:

function greet(name: string): string {
  return `Hello, ${name}!`;
}

const greetSpy = createSpy(greet);

greetSpy("Alice");
greetSpy("Bob");

// After these calls:
// greetSpy.called should be true
// greetSpy.callCount should be 2
// greetSpy.calls should be [["Alice"], ["Bob"]]
// greetSpy.thisContexts should be [undefined, undefined] (assuming global/undefined context)
// greetSpy.results should be [{ type: 'return', value: 'Hello, Alice!' }, { type: 'return', value: 'Hello, Bob!' }]

Example 2:

class Calculator {
  add(a: number, b: number): number {
    return a + b;
  }
}

const calculator = new Calculator();
const addSpy = createSpy(calculator.add);

calculator.add(5, 3); // This needs to be called with the correct 'this' context

// After this call:
// addSpy.called should be true
// addSpy.callCount should be 1
// addSpy.calls should be [[5, 3]]
// addSpy.thisContexts should be [calculator]
// addSpy.results should be [{ type: 'return', value: 8 }]

Example 3:

function mightThrow(): string {
  throw new Error("Something went wrong");
}

const throwSpy = createSpy(mightThrow);

try {
  throwSpy();
} catch (e) {
  // Error caught
}

// After this call:
// throwSpy.called should be true
// throwSpy.callCount should be 1
// throwSpy.calls should be [[]]
// throwSpy.thisContexts should be [undefined]
// throwSpy.results should be [{ type: 'throw', value: new Error("Something went wrong") }]
// throwSpy.getThrowCount() should be 1
// throwSpy.getReturnCount() should be 0

Constraints

  • The createSpy function must be implemented in TypeScript.
  • The spy should correctly handle functions with zero or more arguments.
  • The spy should correctly preserve the this context when the original function is called as a method of an object.
  • The reset() method should fully clear all recorded state without affecting the original function's behavior.
  • The inspection methods (getCall, getThisContext, getReturnCount, getThrowCount) should return accurate information based on past invocations.

Notes

  • Think about how to capture both successful return values and thrown errors. The try...catch block will be your friend here.
  • The this keyword in JavaScript/TypeScript can be tricky. Ensure your spy correctly binds or captures the this context when the original function is called. The apply or call methods on functions might be useful.
  • Consider how to structure the returned spy object to cleanly expose both the callable function and its inspection methods. A common pattern is to return an object that is also a function, or to have the function have properties attached to it. Jest often attaches properties to the mock function itself.
  • You do not need to implement the full Jest API. Focus on the specific functionalities requested in the problem description.
Loading editor...
typescript