Implement a Jest Test Spy for Function Calls
This challenge asks you to implement a test spy using Jest. Test spies are invaluable tools for observing how functions are called during testing, allowing you to verify arguments, call counts, and return values without altering the original function's behavior. Mastering spies is crucial for writing robust and maintainable unit tests.
Problem Description
Your task is to create a reusable Jest test spy that can be attached to an existing function. This spy should record information about every time the original function is called. Specifically, your spy should track:
- The number of times the function has been called.
- The arguments passed to the function on each call.
- The context (
this) in which the function was called on each call.
You will need to provide methods on the spy object to query this recorded information.
Key Requirements:
spyOn(object, methodName): A function that takes an object and the name of a method on that object. It should replace the original method with a spy and return the spy object.spy.calls: An array that stores information about each call. Each element in this array should be an object containing:args: An array of the arguments passed to the function.context: Thethisvalue the function was called with.
spy.callCount: A number representing the total number of times the function has been called.spy.restore(): A method to restore the original function to the object.- Return Value: The spy should not interfere with the original function's return value.
Expected Behavior:
When spyOn is used, the original method should be replaced. Subsequent calls to the method on the object should increment callCount, add details to calls, and execute the original method, returning its result. Calling restore() should bring back the original method.
Edge Cases:
- What happens if the
methodNamedoes not exist on theobject? (For this challenge, assume valid inputs or handle gracefully if you wish.) - Consider functions that are called with no arguments.
- Consider functions called with different
thiscontexts.
Examples
Example 1:
// --- Setup ---
const mockFunction = jest.fn(); // Using Jest's built-in for comparison, your implementation will be different
// Assume a hypothetical spy implementation for demonstration purposes
class Spy {
public calls: Array<{ args: any[], context: any }> = [];
public callCount: number = 0;
private originalMethod: Function;
private object: any;
private methodName: string;
constructor(obj: any, method: string) {
this.object = obj;
this.methodName = method;
this.originalMethod = obj[method];
this.object[this.methodName] = (...args: any[]) => {
this.callCount++;
this.calls.push({ args, context: this });
return this.originalMethod.apply(this, args);
};
}
restore() {
this.object[this.methodName] = this.originalMethod;
}
}
function spyOn(object: any, methodName: string): Spy {
return new Spy(object, methodName);
}
// --- Usage ---
const myObject = {
greet(name: string): string {
return `Hello, ${name}`;
}
};
const greetSpy = spyOn(myObject, 'greet');
myObject.greet('Alice');
myObject.greet('Bob');
// --- Assertions ---
console.log(greetSpy.callCount); // Expected: 2
console.log(greetSpy.calls.length); // Expected: 2
console.log(greetSpy.calls[0].args); // Expected: ['Alice']
console.log(greetSpy.calls[1].args); // Expected: ['Bob']
console.log(greetSpy.calls[0].context === myObject); // Expected: true (assuming Spy is bound correctly)
greetSpy.restore();
console.log(myObject.greet('Charlie')); // Expected: 'Hello, Charlie'
Explanation:
We define an object myObject with a greet method. We then use our spyOn function to create a spy for myObject.greet. When myObject.greet is called twice, the spy records the call count and arguments. Finally, we restore the original method and verify it still works.
Example 2:
// --- Setup (using the same Spy and spyOn from Example 1) ---
const calculator = {
add(a: number, b: number): number {
return a + b;
}
};
const addSpy = spyOn(calculator, 'add');
calculator.add(5, 3);
calculator.add(10, -2);
calculator.add(0, 0);
// --- Assertions ---
console.log(addSpy.callCount); // Expected: 3
console.log(addSpy.calls[0].args); // Expected: [5, 3]
console.log(addSpy.calls[1].args); // Expected: [10, -2]
console.log(addSpy.calls[2].args); // Expected: [0, 0]
console.log(addSpy.calls[0].context === calculator); // Expected: true
Explanation:
This example shows how the spy correctly captures arguments for a numerical function. The context is also verified to be the calculator object.
Example 3: Calling with different this context
// --- Setup (using the same Spy and spyOn from Example 1) ---
class User {
name: string;
constructor(name: string) {
this.name = name;
}
sayHello() {
return `Hello, my name is ${this.name}`;
}
}
const user1 = new User('Alice');
const user2 = new User('Bob');
// Spy on the sayHello method of user1
const sayHelloSpy = spyOn(user1, 'sayHello');
user1.sayHello(); // Called with user1's context
const boundSayHello = user1.sayHello.bind(user2); // Simulate calling with a different context
// Note: Your spy implementation needs to correctly capture the 'this' value
// if the bound function is called. For a standard spyOn, this might require
// careful handling of the original method's apply/call.
// For simplicity in this challenge, we'll assume direct calls or calls where 'this' is preserved.
// If you were to spy on bound functions, it would be a more advanced scenario.
// Let's stick to direct calls for this example for clarity.
// Re-spying on user1 for a cleaner test of context
const user3 = new User('Charlie');
const sayHelloSpy2 = spyOn(user3, 'sayHello');
user3.sayHello();
// --- Assertions ---
console.log(sayHelloSpy2.callCount); // Expected: 1
console.log(sayHelloSpy2.calls[0].context === user3); // Expected: true
Explanation:
This example highlights the importance of the context property. When a method is called on an object, the this keyword inside that method refers to the object itself. The spy correctly captures this this value.
Constraints
- The spy implementation should be written in TypeScript.
- You should not use Jest's built-in
jest.spyOnfunction for your core spy implementation. You are implementing it from scratch. You can usejest.fn()for helper functions if absolutely necessary for testing your spy, but the spy logic itself must be custom. - The spy should work for methods on plain JavaScript objects and class instances.
- The
restore()method must correctly revert the original function.
Notes
- Think about how to intercept the function call. JavaScript's function properties and
apply/callwill be useful. - Ensure your spy correctly handles the original function's arguments and return values.
- The
spyOnfunction should be a standalone function, not a method of a spy class. It should return an instance of your spy class. - Consider how to handle the
thiscontext when calling the original function from within your spy.