Jasmine unit testing: Don't forget to callThrough()

TL;DR

One of the great things about Jasmine, the Javascript unit testing library, is the spy. A spy lets you peek into the workings of the methods of Javascript objects. Just don't forget to use callThrough() when you don't want to alter how the spied-upon function behaves. That's because a spy automatically replaces the spied function with a stub. If you want the spied-upon function to be called normally, add .and.callThrough() to your spy statement.

Jasmine spies are great

One of the great things about Jasmine, the Javascript unit testing library, is the spy. From Jasmine's documentation:

A spy can stub any function and tracks calls to it and all arguments. A spy only exists in the describe or it block in which it is defined, and will be removed after each spec.

They are extremely useful for testing and should be part of every unit test writer's tool set. However, if you forget some basic truths about spies, you will get unexpected results and wonder why your beautiful tests don't work. Some examples:

spyOn(myGreatJSLib, 'doSomething');
myGreatJSLib.doSomething();
// passes
expect(myGreatJSLib.doSomething).toHaveBeenCalled();

spyOn(myGreatJSLib, 'doAnotherThing');
myGreatJSLib.doAnotherThing('value1', 10);
// also passes
expect(myGreatJSLib.doAnotherThing).toHaveBeenCalledWith('value1', 10);

spyOn() replaces functions

Jasmine's documentation adds:

By chaining the spy with and.callThrough, the spy will still track all calls to it but in addition it will delegate to the actual implementation.

This is an important piece of information to keep in mind, because, depending on what you're testing, your tests will appear to be working correctly, but your expect statements might fail unexpectedly.

Imagine we're testing our awesome RickAndMortyLib which has a getQuote(characterName) function. Supplied with the values Rick, Morty, or Summer, the function will return a quote for that character. For the sake of this example, there is only one quote per character:

  • Rick: "Oh yeah, you gotta get schwifty."
  • Morty: "Don't even trip about your pants, dawg. We got an extra pair right here."
  • Summer: "God, Grandpa, you're such a dick."

Some examples:

spyOn(RickAndMortyLib, 'getQuote');
RickAndMortyLib.getQuote('Rick');
// passes
expect(RickAndMortyLib.getQuote).toHaveBeenCalledWith('Rick');

// but....
var rickQuote = RickAndMortyLib.getQuote('Rick');
// FAILS
expect(rickQuote).toBe('Oh yeah, you gotta get schwifty.');

Why did that last expect fail? Because the spyOn() call up top replaces getQuote() with a stub which can only confirm that it was called and/or called with specific arguments. We could fake a response with one of these two techniques:

spyOn(RickAndMortyLib, 'getQuote').and.returnValue(
                   'Morty, can you get to the left nipple?');
var rickQuote = RickAndMortyLib.getQuote('Rick');
// passes
expect(rickQuote).toBe('Morty, can you get to the left nipple?');

// or....
spyOn(RickAndMortyLib, 'getQuote').and.callFake(function() {
    return 'GRAAAAAASSSSSSS....tastes bad.'
});
// passes
expect(rickQuote).toBe('GRAAAAAASSSSSSS....tastes bad.');

Those two tests pass, and the and.returnValue() and and.callFake() calls can be very useful in situations where it's impossible or difficult to supply whatever your library needs to complete the function call on its own. However, know that you're not really testing the function itself when you use a plain spyOn() call or when you chain and.returnValue() or and.callFake().

To really test the function call itself, you need to add and.callThrough():

spyOn(RickAndMortyLib, 'getQuote').and.callThrough();
RickAndMortyLib.getQuote('Rick');
// passes
expect(RickAndMortyLib.getQuote).toHaveBeenCalledWith('Rick);
var rickQuote = RickAndMortyLib.getQuote('Rick');
// now this expect passes
expect(rickQuote).toBe('Oh yeah, you gotta get schwifty.');