lightbulb logo

Mock object testing with jsTest

This article picks up where we left off in the first article on jsTest. Everything we cover in this article can also be applied to the jsTest Eclipse continuous testing plug-in. Last time we looked at how to write unit tests using assertions in jsTest. This time we'll look at mock objects, which are useful when tests deal with interactions between objects. To understand some problems with testing using assertions alone, consider this example (borrowed from Wikipedia).

An alarm clock program causes a bell to ring at some predetermined time. The program must somehow get the time from the outside world and then determine whether the bell should ring. It would be quite inconvenient to have to wait until the alarm time to see if the program works (anyone who has bought a new alarm clock will know this). It may also be undesirable to change the computer's time in order to test the program. Maybe another program relies on the system time always being correct.

So what can we do? Well if we represent time with an object and somehow have the alarm clock code determine the time by querying this object, we can test it by giving it a fake object which will report whatever times we program into it. That is all a mock object really is. It's a test object that mimics a real object for the purposes of testing.

Furthermore, getting back to the example, we don't want a potentially loud and distracting bell to ring every time we test the alarm clock code. So instead we can hook the alarm clock up to something else, just to see if it works. And for that we can use another mock object.

Lets get started. This is Test Driven Development so the first thing we should do is write a test. It can sometimes be hard to think what the very first test should be. Lets start by testing that when we tick the alarm clock, it rings. Obviously we don't want it to ring all the time but we aren't finished yet and this seems like a reasonable goal for one iteration. Here's a test.

// alarm.test.js

Test.requires("alarm.js"); 

var bell = new Mock(); 
var alarmClock = new AlarmClock(); 

function test_alarm_clock_rings_when_ticked() 
{ 
  bell.expect.ring(); 
  alarmClock.tick(); 
}

I've done a few extra things here. First line 3 pulls in the contents of the file alarm.js so that we can implement the alarm code itself in a separate file from the tests. Line 6 creates the new AlarmClock object that we will be testing. Line 5 creates a new bell. But notice that it isn't a real bell. It's a Mock.

Line 10 is where the interesting stuff happens. Here we are setting an expectation on the mock bell. Line 10 could be translated as, "I expect the ring function of the bell to be called".

And then on line 11 we simply tick the alarm clock. By the way, I'm going to assume that the alarm clock gets ticked several times per minute and on each tick it will poll the current time and either call ring to continue ringing the bell or not to stop it.

Run the tests! They will fail but it doesn't matter! It takes some getting used to but you should always run your test and see it fail before you write the code that will make it pass. Let's see how badly it fails...

alarm.test.js(3): ReferenceError: "AlarmClock" is not defined.

Crash and burn! But it's okay, now we can "lean on the compiler". Actually JavaScript is not usually a compiled language so in this case we'll have to "lean on the unit testing tool". It failed because we haven't made an AlarmClock class yet. But let's not get carried away. We write only enough code to fix the immediate problem.

// alarm.js

function AlarmClock()
{
}

That was easy! If you haven't used objects in JavaScript before it may seem a little strange that I defined AlarmClock to be a function. But that's how JavaScript works. Run the tests again...

alarm.test.js(8): Expected call ring()
alarm.test.js(9): TypeError: Cannot find function tick. 

That's more like it. It's telling us that the bell function wasn't called as the test expected. It also reports that we don't have a tick function in our AlarmClock. Let's fix those two problems.

// alarm.js

function AlarmClock(bell)
{
  this.tick = function()
  {
    bell.ring();
  }
}

We have to make the bell ring but the AlarmClock does not know about the bell yet. So I added it as a parameter and fixed the test, as you'll see in a moment. Run the tests again and everything passes. Now we can continue. This is the point at which you would refactor to clean up your code. I don't see anything smelly yet though.

We need another test. Obviously we don't want the alarm clock to ring all the time. If it hasn't reached the time we set for it, it shouldn't ring. Let's test that.

// alarm.test.js

Test.requires("alarm.js"); 

var timeSource = new Mock(); 
var bell = new Mock(); 

var alarmClock = new AlarmClock(bell); 
alarmClock.setAlarmTime(17, 35); 

function test_alarm_clock_does_not_ring_before_requested_minute() 
{ 
  timeSource.expect.getMinutes().andReturn(34); 
  bell.expect.ring().times(0); 
  alarmClock.tick(); 
} 

function test_alarm_clock_rings_when_ticked() 
{ 
  bell.expect.ring(); 
  alarmClock.tick(); 
}

I added a new mock object to represent the source of the time. Remember that we don't want to go changing the system clock to test this code. The alarm clock is set to 17:35 before we run each test. Then I added a new test to check that the alarm clock does not ring the bell at 34 minutes past the hour. I do that by setting an expectation on the mock time source. Line 13 could be translated as, "I expect the time source to return 34 minutes when its getMinutes function is called". Line 14 is another interesting expectation. It says, "I expect the function ring of bell to be called zero times". Or in other words, "I expect that ring will not be called". And the result is...

alarm.test.js(5): TypeError: Cannot find function setAlarmTime.

That's easy to fix. But be careful not to right more code that is necessary. Notice that we're still leaning on the compiler. The error messages tell us what to do next. At this stage of the iteration we're like robots. The creative and interesting programming happens when we write new tests and refactor our code to improve its design.

// alarm.js

function AlarmClock(bell)
{
  this.setAlarmTime = function(hours, minutes)
  {
  }

  this.tick = function()
  {
    bell.ring();
  }
}

I added the setAlarmTime function but I didn't give it a body because I don't need to yet. Let's see if that helps...

alarm.test.js(10): Expected call getMinutes()
alarm.test.js(12): Unexpected call to ring()

It's telling us that our alarm clock code does not call getMinutes as we said it would. Then it says that it called ring when we said it wouldn't. Let's fix that. We should only call ring if the current minutes are at the time set on the alarm clock.

// alarm.js

function AlarmClock(timeSource, bell)
{
  this.setAlarmTime = function(hours, minutes)
  {
    this.minutes = minutes;
  }

  this.tick = function()
  {
    var currentMinutes = timeSource.getMinutes();
    if (currentMinutes == this.minutes)
      bell.ring();
  }
}

I added another parameter to the AlarmClock so that we can pass a reference to the time source. Then I added the logic to only ring the bell if the minutes are at the set time. Running the tests gives...

alarm.test.js(17): Expected call ring()

We've broken the very first test we wrote. Why is that? The alarm code now calls getMinutes to determine whether the bell should ring but the failing test doesn't set up an expectation for it to be called. Also we should change the name because now it shouldn't ring every time it is ticked.

// alarm.test.js

Test.requires("alarm.js");

var timeSource = new Mock();
var bell = new Mock();

var alarmClock = new AlarmClock(timeSource, bell);
alarmClock.setAlarmTime(17, 35);

function test_alarm_clock_does_not_ring_before_requested_minute()
{
  timeSource.expect.getMinutes().andReturn(34);
  bell.expect.ring().times(0);
  alarmClock.tick();
}

function test_alarm_clock_rings_when_minutes_are_at_set_time()
{
  timeSource.expect.getMinutes().andReturn(35);
  bell.expect.ring();
  alarmClock.tick();
}

And now all the tests pass. I think the next thing we should do is make the alarm clock take the hours into account. We don't want it to ring at 18:35 if we set it to 17:35. We could add an hours parameter to setAlarmTime and add a getHours function to the time source. But there is some redundancy there. Really we want to represent times with objects. Let's refactor. I did this in several steps and I made a few mistakes along the way. But I knew when I was finished because the tests passed. First alarm.js with the new Time objects.

// alarm.js

function Time(hours, minutes)
{
  this.minutes = minutes;
}

function AlarmClock(timeSource, bell)
{
  this.setAlarmTime = function(time)
  {
    this.alarmTime = time;
  }

  this.tick = function()
  {
    var currentTime = timeSource.getTime();
    if (currentTime.minutes == this.alarmTime.minutes)
      bell.ring();
  }
}

And here are the refactored tests.

// alarm.test.js

Test.requires("alarm.js");

var timeSource = new Mock();
var bell = new Mock();

var alarmClock = new AlarmClock(timeSource, bell);
alarmClock.setAlarmTime(new Time(17, 35));

function test_alarm_clock_does_not_ring_before_requested_minute()
{
  timeSource.expect.getTime().andReturn(new Time(17, 34));
  bell.expect.ring().times(0);
  alarmClock.tick();
}

function test_alarm_clock_rings_when_minutes_are_at_set_time()
{
  timeSource.expect.getTime().andReturn(new Time(17, 35));
  bell.expect.ring();
  alarmClock.tick();
}

I shouldn't have introduced the hours parameter so early. I didn't need it. But we all make mistakes. Notice that I didn't change what the code does. That is one of the rules of refactoring. When you're finished, the code shouldn't do anything it didn't do before. Only the quality of the design changes (hopefully for the better).

Now we're finished refactoring, we can test that the alarm clock respects the current hour.

// alarm.test.js

Test.requires("alarm.js"); 

var timeSource = new Mock(); 
var bell = new Mock(); 

var alarmClock = new AlarmClock(timeSource, bell); 
alarmClock.setAlarmTime(new Time(17, 35)); 

function test_alarm_clock_does_not_ring_before_requested_minute() 
{ 
  timeSource.expect.getTime().andReturn(new Time(17, 34)); 
  bell.expect.ring().times(0); 
  alarmClock.tick(); 
} 

function test_alarm_clock_does_not_ring_before_requested_hour() 
{ 
  timeSource.expect.getTime().andReturn(new Time(16, 35)); 
  bell.expect.ring().times(0); 
  alarmClock.tick(); 
} 

function test_alarm_clock_rings_when_minutes_are_at_set_time() 
{ 
  timeSource.expect.getTime().andReturn(new Time(17, 35)); 
  bell.expect.ring(); 
  alarmClock.tick(); 
}

The test fails. I'm sure you can guess why! Let's fix it.

// alarm.js

function Time(hours, minutes)
{
  this.minutes = minutes;
  this.hours = hours;
}

function AlarmClock(timeSource, bell)
{
  this.setAlarmTime = function(time)
  {
    this.alarmTime = time;
  }

  this.tick = function()
  {
    var currentTime = timeSource.getTime();
    if (currentTime.minutes == this.alarmTime.minutes && currentTime.hours == this.alarmTime.hours)
      bell.ring();
  }
}

I had to add an hours field to the Time class and I changed the tick logic so it compares the hours before ringing the bell. Another code smell! Let's refactor...

// alarm.js

function Time(hours, minutes)
{
  this.minutes = minutes;
  this.hours = hours;

  this.equals = function(rhs)
  {
    return this.minutes == rhs.minutes && this.hours == rhs.hours;
  };
}

function AlarmClock(timeSource, bell)
{
  this.setAlarmTime = function(time)
  {
    this.alarmTime = time;
  }

  this.tick = function()
  {
    var currentTime = timeSource.getTime();
    if (currentTime.equals(this.alarmTime))
      bell.ring();
  }
}

I extracted the comparison code out of the tick function because I thought it belonged in the Time class.

This seems like a good place to stop. At this point, the bell will start to ring at the set time and continue to ring for one minute. Next time we'll make it keep ringing until someone hits the stop button.

I hope that you better understand some of the philosophy of TDD from this article. If you haven't tried it before you could give it a shot. And of course, I hope you learned more about testing with mock objects.