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
describeoritblock 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.');
6 comments
Comments are closed. These were left on the previous version of this site. Questions? Get in touch.
Hi Federico,
Thank you for your article, really clear.
One question: If I'm going to test the service itself (using .and.callThrough()), What is the advantage of spy on it instead of calling it directly?
Thanks
Hi Aleix.
You can certainly just call a service directly and examine the return value. Using spies, however, gives you more information about the call. For example, only with a spy can you use
expect(foo).toHaveBeenCalled()orexpect(bar).toHaveBeenCalledWith('hi').Hi Federico,
Thank you for your article. I tried same way but promise is not resolving and not getting any data.
spyOn(RickAndMortyLib, 'getQuote').and.callThrough();
RickAndMortyLib.getQuote();
var tee1 = RickAndMortyLib.getQuote().then(function(response) {
console.log('Success', response);
});
scope.$apply();
.and.callThrough() is not resolving to success
Hi Federico,
Thank you for your article.
I used same way for callThrough(). But it not resovling my status.
spyOn(myService, 'gettest1').and.callThrough();
myService.gettest1();
var tee1 = myService.gettest1().then(function(response) {
console.log('Success', response);
});
scope.$digest();
It not showing console Success response. But I am getting data in controller not in It block. I am using Jasmine2. Can you help me in this to get actuall data from API
Hi Federico,
I really appreciate this article to give a crystal clear understanding of the callThrough method. I just been through a lot of videos and articles but was not able to understand the exact implementation of the method.
your article is SUPER AWESOME!!!!
Thank you, Chaynnitt! Glad you found the article useful. 😀