How function spies work in JavaScript


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.

Testing framework suggestions? Try Sinon or Jasmine.

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…

The Code

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

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

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?

Related

Spying Constructors in JavaScript

2 Comments

·

Leave a Reply

  1. 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

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s