Previous: part 5
You can also find the code below in the src/unit-test-4 dir of my blog code repository.
In the previous post, we talked about spying. With a test spy, you can spy on method calls and see how they are called. This is a bit like listening in on a conversation passively.
The next step is to provide an answer to a method call, similar to interrupting someone who is about to answer a question, and giving your own – different – answer.
This is done via a test stub. Let’s look at an example. You have a website with users, and you want to show the current weather for the logged in user. To do that, you have created a simple function:
function generateWeatherReport(user) { var city = user.location.city; var countryCode = user.location.countryCode; return weatherService.fetchLocalWeather(city, countryCode) .then(function(weatherData) { return sprintf('The weather in %s, %s is %s.', city, countryCode, weatherData.summary); }); }
In goes a user, out comes the one-line weather report. Your helper function fetchLocalWeather of the weatherService calls a weather website’s API to fetch the local weather.
How can we test this?
require('should'); var weather = require('./weather'); describe('generateWeatherReport', function() { it('should generate a weather report', function(done) { var user = { location: { city: 'Amsterdam', countryCode: 'NL' } }; weather.generateWeatherReport(user) .then(function(report) { report.should.equal('The weather in Amsterdam, NL is rainy, cold and miserable..'); done(); }) .catch(function(err) { done(err); }); }); });
This code runs. However, the test only succeeds when it is actually rainy, cold and miserable in Amsterdam. Admittedly, this is a large part of the year, but it would be better if this test also succeeds when it’s sunny in Amsterdam.
Again, the sinon module comes to the rescue. With it, we can stub the fetchLocalWeather call, and make it return what we want for a given input. Like this:
require('should'); var sinon = require('sinon'); var Q = require('q'); var weather = require('./weather'); var weatherService = require('./weather.service'); describe('generateWeatherReport', function() { it('should generate a weather report', function(done) { var user = { location: { city: 'Amsterdam', countryCode: 'NL' } }; var fetchLocalWeatherStub = sinon.stub(weatherService, 'fetchLocalWeather'); fetchLocalWeatherStub.withArgs('Amsterdam', 'NL') .returns(Q.resolve({ summary: 'sunny and great' })); weather.generateWeatherReport(user) .then(function(report) { report.should.equal('The weather in Amsterdam, NL is sunny and great.'); done(); }) .catch(function(err) { done(err); }) .done(function() { fetchLocalWeatherStub.restore(); }); }); });
It’s necessary to import the weatherService in the test file, even though we are not using it directly. Because imported modules in node.js are only imported once, and then behave like a singleton, the weatherService in the spec file is the same as in the weather.js file.
Using sinon, we overwrite the fetchLocalWeather function. If it’s called with arguments Amsterdam, NL , we return a fixed value.
Note that it’s important to restore this stub when you’re done with this test, otherwise fetchLocalWeather for Amsterdam, NL will keep returning this value for other tests in your test suite.
Now if only we could improve the actual weather in Amsterdam with sinon!