Spying while point-free programming | Toucan Toco

Categories :

Table of Contents

At Toucan Toco we rely heavily on CoffeeScript, lodash and d3 and we’re big fans of point-free style.

We’re also a lot into testing. However we just hate testing the same stuff several times. This is why we use sinon spies in order to assert that our higher-order functions are correctly calling the lower ones.

Let’s take a very simple example. It’s about testing functions that returns other functions. This is a simple d3-style component:

// The 'constructor'
MyComponent = function(...) {

  // The instance, called at each update
  myComponent = function(...) {
    ...
  }

  return myComponent;
}

When we start to factor components, we can usualy reach the point where MyComponent will delegate some tasks to a smaller component. Let’s call it MySubComponent.

// The 'constructor'
MyComponent = function(...) {
  mySubComponent = MySubComponent(...)

  // The instance, called at each update
  myComponent = function(...) {
    ...
    mySubComponent(...)
    ...
  }

  return myComponent;
}

If MySubComponent is correctly tested, I don’t want to repeat all these tests in MyComponent. So instead I’d like to create a spy to test that the higher component is calling the lower one with the correct arguments.

Testing the call in the constructor is easy:

it('should call MySubComponent when instanciating MyComponent', function() {
  MySubComponentSpy = sinon.spy(MySubComponent);
  ...
  myComponent = MyComponent(...);

  MySubComponentSpy.should.be.called;
}

But how can we test the instance call ? We don’t have any mySubComponentSpy (spy on the instance, i.e. the return of the MySubComponent constructor).

it('should call MySubComponent when instanciating MyComponent', function() {
  MySubComponentSpy = sinon.spy(MySubComponent);
  ...
  myComponent = MyComponent(...);
  MySubComponentSpy.should.be.called;

  myComponent(..);
  mySubComponentSpy.should.be.called; // We would like to do that!
}

If we look deeper into MySubComponentSpy, we’ll notice that sinon exposes the return value of the calls to the spies: MySubComponentSpy.returnValues. However, adding a spy on this wouldn’t replace the value that is stored in our myComponent instance, so it won’t get called afterwards :’( Furthermore we don’t want to add nasty setters everywhere just for the sake of testing.

So let’s modify how we create a spy to decorate a function that returns a function by a spy:

function spyReturnedValue(f) {
  // When I "deep spy" a function...
  return sinon.spy(function() { // ... I instead spy on a function...
    return sinon.spy(f.apply(this, arguments)); // ...that returns a spy on it's return value
  });
})

Still following? ;)

Now we can do:

it('should call MySubComponent when instanciating MyComponent', function() {
  MySubComponentSpy = spyReturnedValue(MySubComponent);
  ...
  myComponent = MyComponent(...);
  MySubComponentSpy.should.be.called;

  myComponent(..);
  mySubComponentSpy = MySubComponentSpy.returnValues[0]; // Now this is a spy!
  mySubComponentSpy.should.be.called;
}

While point-free programming, you functions are curried, which means you’ll find yourself having functions that return functions that return functions…

To be able to spy any level of returned functions, let’s recurse our spyReturnedValue function:

function spyReturnedValue(f) {
  if (typeof f is 'function') { // This will break the recursion
    return sinon.spy(function() {
      return spyReturnedValue(f.apply(this, arguments)); // Go for one more round baby!
    });
  } else {
    return f;
  }
})

And that’s it! We’re now are able to spy on our beautiful curried functions, like f = (a) -> (b) -> (c) -> (d) -> ... It’s beautiful in CoffeeScript, isn’t it ?

Hope you enjoyed this little ride deep in this NSA-class spying system.

If you’d like to read more tips about how we do tests at Toucan Toco, read Sophie’s article about rewire and how to spy and mock your required dependencies.

This is a heading 2

This is a paragraph

This is a heading 2

This is a paragraph

Table of Contents