In the previous post we had a look at sinon spies. With spies, we are able to determine if a specific function was called or not. Usually the dependencies between units are more interesting, they involve units co-operating, exchanging data and so on. Spies do not suffice. Let’s have a look at another technique, using stubs.
A stub is pre-programmed function. You define the expected input and output and the stub complies happily. If you call the stub in an unexpected way, it will typically return a default value (undefined
). In code, this looks something like that:
// create a stub
var stub = sinon.stub();
// prints 'undefined', nothing programmed so far
console.log(stub());
// program the stub to return hello
stub.returns('hello');
// prints 'hello'
console.log(stub());
// program the stub to return hello world for specific arguments
stub.withArgs('world').returns('hello world');
// prints 'hello world'
console.log(stub('world'));
Let’s see how we can use a stub in our Calculator example. We are going to add a new dependency, a battery. The calculator will offer a new feature that will check the battery’s charge level. If it’s above zero, then the calculator will report that it is ready to work.
Before we start writing the unit test, we already know that the Battery
will be implemented as an object that has a method called getLevel
. The method returns a number between 0 and 100. That’s the interface (or contract) of the Battery.
The first unit test looks like this:
it('should be ready when the battery is charged', function() {
// arrange
var bell = sinon.spy();
var battery = {
getLevel: sinon.stub()
};
battery.getLevel.returns(100);
var calculator = new Calculator(bell, battery);
// act
var isReady = calculator.isReady();
// assert
expect(isReady).to.be.true;
});
The Calculator
constructor now takes a second parameter, the battery. The bell is not really used in this test, but we should pass a parameter. In the act step, you can see the new method of the calculator, isReady
, which is expected to return true
in this test.
The setup of the test shows that the battery is a simple object with a function called getLevel
. Then we stub that function to return the value 100.
The implementation of the Calculator
changes like this:
// changed: new parameter for the battery
function Calculator(bell, battery) {
this.bell = bell;
this.battery = battery;
}
// new method: isReady
Calculator.prototype.isReady = function() {
return this.battery.getLevel() > 0;
};
Are we done? Not really. First of all, we only added one unit test that proves the calculator reports it is ready when the battery is charged. We should cover the opposite case as well: the calculator should report it’s not ready when the battery is empty. Note that even if we don’t cover this case, code coverage will be reported as 100% (remember how code coverage doesn’t say anything about the quality of your tests).
To cover the negative test case, we could copy paste the test we already wrote and change it slightly:
it('should not be ready when the battery is empty', function() {
// arrange
var bell = sinon.spy();
var battery = {
getLevel: sinon.stub()
};
battery.getLevel.returns(0);
var calculator = new Calculator(bell, battery);
// act
var isReady = calculator.isReady();
// assert
expect(isReady).to.be.false;
});
This works, but we can avoid some copy pasting by moving some of the common arrange steps into a shared step. Mocha offers hooks that you can run before or after all tests. Let’s see both tests together with this technique:
describe('isReady', function() {
var battery;
var calculator;
beforeEach(function() {
// arrange
var bell = sinon.spy();
battery = {
getLevel: sinon.stub()
};
calculator = new Calculator(bell, battery);
});
it('should be ready when the battery is charged', function() {
// arrange
battery.getLevel.returns(100);
// act
var isReady = calculator.isReady();
// assert
expect(isReady).to.be.true;
});
it('should not be ready when the battery is empty', function() {
// arrange
battery.getLevel.returns(0);
// act
var isReady = calculator.isReady();
// assert
expect(isReady).to.be.false;
});
});
All the common setup parts are moved into a beforeEach
function. This is a hook offered my mocha and it is executed before any unit test within the same describe
scope. Mocha also offers a before
hook that runs once at the beginning of the describe
scope. before
runs only once, beforeEach
runs once per unit test. There are also their counterparts, after
and afterEach
that run after the unit tests have run.
Moving the setup parts into a hook is a good practice because it isolates the plumbing that doesn’t change. The Calculator is always going to use the Battery (and the Bell) in the same way, that doesn’t change. What changes per unit test is the battery’s state. This also makes the unit tests very clear, in which the arrange step is a one liner.
A subtle yet important difference of these examples, compared to the spies examples, is that the assertion step is done on the calculator itself, our SUT (system under test). In the previous post, the assertion was checking that the spy was called. In these examples, the assertion is checking that the Calculator returned the correct value. We don’t really care what the Calculator is doing behind the scenes. All we care is that, given a full battery (the part we stub), the Calculator will report it is ready (and vice versa).
Why didn’t we do it like this in the previous post with the bell? Well, the bell did not return any values. In functional programming, this is called a side effect: all the bell does is interact with the outside world, but it doesn’t return a value. A similar example is a function that writes something to the console or to a disk file. For these cases, the unit test has to use a spy to verify that the implementation is called. This goes against the principle of testing behavior and not implementation, but for side-effects there is no alternative. For the battery example however, we just test the behavior. We don’t care about the implementation, we don’t care if the getLevel
function is called or not.
Finally, let’s see a different way of using the stub. This can come in handy if you already have an implementation of the Battery class:
// instead of this
battery = {
getLevel: sinon.stub()
};
// you can do this
battery = require('./BatteryImplementation');
sinon.stub(battery, 'getLevel');
This has an extra benefit: if the contract of the Battery is modified (e.g. the method is renamed to getChargeLevel) then the unit test will break because sinon will complain that it can’t stub a non existing function. This is another layer of safety. Even if you don’t have concrete implementations, it’s a good idea to separate your definitions into separate files.
In the next post, we’ll have a look at the final technique from sinon, mocks, and we’ll see how they’re different to stubs.