If you write unit tests, then you likely use a testing framework and might have come across spies. If you don’t write unit tests, please take a quick pause and promise yourself to always write tests.
Spies allow you to monitor a function; they expose options to track invocation counts, arguments and return values. This enables you to write tests to verify function behaviour.
They can even help you mock out unneeded functions. For example, dummy spies can be used to swap out AJAX calls with preset promise values.
The code below shows how to spy on the bar method of object foo.
spyOn(foo, 'bar');
foo.bar(1);
expect(foo.bar).toHaveBeenCalled();
expect(foo.bar).toHaveBeenCalledWith(1);
Jump into the documentation for more examples.
That was pretty cool right. So how difficult can it be to write a spy and what happens under the hood? It turns out implementing a spy is very easy in JavaScript. So let’s write ours!
The goal of the spy is to intercept calls to a specified function. A possible approach is to replace the original function with another function that stores necessary information and then invokes the original method. Partial application makes this quite easy…
Sample Spy implementation in JavaScript
function Spy(obj, method) {
let spy = {
args: []
};
let original = obj[method];
obj[method] = function() {
let args = [].slice.apply(arguments);
spy.count++;
spy.args.push(args);
return original.call(obj, args);
};
return Object.freeze(spy);
}
let sample = {
fn: function(args){
console.log(args);
}
};
let spy = Spy(sample, 'fn');
sample.fn(1,2,3);
console.log(spy.args.length); //1
console.log(spy.args); //[[1,2,3]]
sample.fn('The second call');
console.log(spy.args.length); //2
console.log(spy.args); //[[1,2,3], 'The second call']
//try modifying the spy
spy.args = [];
console.log(spy.args); //[[1,2,3], 'The second call']
Taking the code apart
The spy method takes an object and a method to be spied upon. Next, it creates an object containing the call count and an array tracking invocation arguments.
It swaps out the original call with a new function that always updates the information object whenever the original method is invoked.
The Object.freeze call ‘freezes’ the spy object and prevents any modifications of values. This is necessary to prevent arbitrary changes of the spied values.
Limitations of Test Spies
The toy sample is brittle (yes I know it). Can you spot the issues? Here are some:
- What happens if the method doesn’t exist on the object?
- What happens if the object is null?
- Can it work for non-object methods? Would pure functions work? Would using window as the parent object work?
- What happens if method is a primitive and not a function?
These can (and should) be fixed but again, that would make this post very complicated. The goal was to show a simple toy implementation.
Challenges with unregistering Spies
How do you ‘unregister’ spies without losing the original method? Hint: store it in some closure and replace once you expose an unregister call.
How would you implement a spy in Java?
On line 11, you need to use .apply instead of .call.
Change sample.fn to
let sample = {
fn: function(a, b, c){
console.log(`${a} and ${b} and ${c}`);
}
};
to see the difference
LikeLike
Thanks! Good call – was a mistake on my part.
LikeLike