I’ve been getting much more involved in JavaScript development over the last few months and I have been playing around with QUnit as a unit testing framework. I’m a big believer in unit testing - so much so that I now find it actively painful to check in code without unit tests - and for the first few tests I wrote it seemed to do everything I needed. I didn’t really need any mock or stub objects, and even when I did, I could generally get away with just replacing the method on the original object:
//WARNING: BAD CODE
test('check that POST is called', function() {
//mock up a call to $.post and save the parameters
var postUrl, postCallback;
$.post = function(url, callback) {
postUrl = url;
postCallback = callback;
}; //run the method under test
doSomething(); //check the saved variables
equal('/some/url', postUrl); //use the callback somehow
postCallback({ success: true });
});
This got me through my basic needs but it is a pretty messy way of getting the desired effect. Whenever I’m coding in C# I use the fantastic Moq mocking library and I was sure that there was something out there with a similar syntax and functionality for JavaScript.
I had been looking for a “real” project to drive my JS learning, and this sounded like an interesting idea: the dynamic nature of JavaScript means that there’s none of the IL-injection pain associated with mock frameworks… why not write one myself?
Requirements
What do I actually need out of my mocking framework? If I model it on Moq then I want to be able to:
- Register an expected call to a function
- Set expected parameters
- Specify how many times it should be called
- Hook up callbacks to be executed
- Verify that all of the above actually took place Let’s look at this step by step:
Setting Up Calls
When building my Mock object, setting up a call will:
- Store a record of the fact that the call has been setup
- Add a function to the mock that masquerades as the actual method, finds the matching setup and notify that it was called if we assume that a Setup class exists that will do all of the parameter matching, callback handling and other magic, then storing the setups becomes pretty simple:
jsMock.Mock = function() {
var _self = this, //array to store all setups on this mock
_setups = [], //sets up a new call
_setup = function(member) {
var setup = new jsMock.Setup(member);
_setups.push(setup);
return setup;
};
this.setup = _setup;
};
This can then be called by:
var mockObject = new jsMock.Mock();
mockObject.setup('post');
Next, we need to add a function to the Mock object that can be called as if it were the real version of the method that we set up:
mockObject.post('url', function() {
//...
});
JavaScript makes this surprisingly easy - all we need to do is create a function that will go through the list of setups recorded so far and find a match. We can then add this function to the Mock object:
//creates a function that will locate a matching setup
_createMockFunction = function(member) {
return function() {
for (var i = 0; i < _setups.length; i++) {
if (_setups[i].member === member && _setups[i].matches(arguments)) {
//notify the setup that it was called
}
}
};
}; //sets up a new call
_setup = function(member) {
var setup = new jsMock.Setup(member);
_setups.push(setup);
this[member] = _createMockFunction(member);
return setup;
};
Note that I have also assumed that our Setup object has a ‘matches(arguments)’ method that will check the arguments passed into the mocked method against those that have been configured.
Verifying Calls
For the Mock object, verification really just means going through each of the configured setups and verifying them, so the implementation is pretty simple:
//verify all of our setups
_verify = function() {
for (var i = 0; i < _setups.length; i++) {
_setups[i].verify();
}
};
this.verify = _verify;
Great - that covers the Mock itself. Now lets move on to the Setup object, where all of the magic happens.
The Setup Object
The Setup object is where the code actually starts to do something. We can start out pretty simple in terms of requirements: we need to be able to match a call to the Mock against a Setup. This means that the Setup needs to store the member name that it is mocking, and (optionally) the parameters.
We can take the member name as a constructor parameter and expose it through a property:
jsMock.Setup = function(member) {
this.member = member;
};
To setup the parameters we would ideally like to be able to pass them into a method with as little extra syntax as possible:
mockObject.setup('post').with('/some/url', function() {
/*...*/
});
To this end, let’s add a ‘with’ method that stores the arguments that are passed in:
//store any specified parameters
_expectedParameters,
//register expected parameters
(_with = function() {
_expectedParameters = arguments; //store the arguments passed into this method
});
this.with = _with;
This approach is fine for simple types (like the string URL) but what about the callback that we expect to be passed into our ‘post’ method? We can’t possibly know what that will be when we setup the mock method, so instead of trying to match it, let’s add a constant that we can recognise to mean “anything”:
mockObject.setup('post').with('/some/url', jsMock.constants.anything);
Now that we’ve created the method to register expected parameters, let’s write something to match against actual parameters.
//checks that the params object matches the expected parameters
_matches = function(params) {
//if expected parameters haven't been specified, match everything
if (_expectedParameters === null) return true;
//same number of parameters?
if (_expectedParameters.length !== params.length) return false;
//do all parameters match?
for (var i = 0; i < _expectedParameters.length; i++) {
if (
_expectedParameters[i] !== jsMock.Constants.anything && //ignore the 'anything' constant
_expectedParameters[i] !== params[i]
)
return false;
}
//it must be a match
return true;
};
this.matches = _matches;
In this method we first check whether or not any parameters have actually been specified for this Setup, then check the number of parameters and finally compare each parameter in turn (ignoring the ‘anything’ constant).
Once we’ve matched a Setup we’ll want to notify that it has been called, so let’s add a ‘called’ method and update our Mock class to call this when it finds a match:
//jsMock.Setup:
//notifies this setup that it has been called
_called = function(params) {};
this.called = _called;
//jsMock.Mock:
_createMockFunction = function(member) {
return function() {
for (var i = 0; i < _setups.length; i++) {
if (_setups[i].member === member && _setups[i].matches(arguments)) {
_setups[i].called(arguments);
}
}
};
};
Now that we have an object that we can match against calls, let’s look at how to configure our expectations for that method.
Expectation Management
For our Setup to be of any use we need to be able to do more than just register that it occurred. Specifically, we want to be able to:
- Specify a return value
- Specify the number of times it should be called
- Specify callbacks that will be executed when it is called
Return Values
Setting up a return value should be configured using something like the following:
mockObject
.setup('add')
.with(1, 2)
.returns(3);
So let’s add a ‘returns’ method that just stores the value passed in.
//set up a return value
_returns = function(returnValue) {
_self.returnValue = returnValue;
return _self;
};
this.returnValue = null;
Note that we are returning ’_self’ to allow the Setup methods to support the fluent interface.
Now we need to update our Mock object so that the mock function returns the return value from the Setup. This is slightly more complicated than it sounds as it is possible to match multiple Setups with a single call. For simplicity, let’s state that the last Setup that has been configured will set the return value that will be used; now we can update our fake method in the Mock:
//creates a function that will locate a matching setup
_createMockFunction = function(member) {
return function() {
var match; //reverse traversing the list so most recent setup is used as match
for (var i = _setups.length - 1; i >= 0; i--) {
if (_setups[i].member === member && _setups[i].matches(arguments)) {
if (!match) match = _setups[i];
_setups[i].called(arguments);
}
}
return match.returnValue;
};
};
Note that we are traversing the list of setups in reverse order to make sure we use the most recent matching Setup.
Expected Number of Calls
When we specify the number of calls we expect, we really want to be able to specify a range: “no more than 3” or “at least 2”. In the interests of creating a more fluent API, let’s put all of the time-specification methods within a ‘times’ object so that we can set these up using something like the below:
mockObject.setup('post').times.noMoreThan(3);
Ideally (and stealing from Moq syntax), we want the following options:
- once - exactly one call
- never - zero calls
- noMoreThan - up to [num] calls
- atLeast - [num] or more calls
- exactly - exactly [num] calls To achieve this, let’s set up an object to store the number of expected calls and a series of methods to set those properties:
//store expected call counts
(_expectedCalls = { min: 0, max: Nan }),
(_times = {
exactly: function(num) {
_expectedCalls.min = _expectedCalls.max = num;
return _self;
},
once: function() {
return exactly(1);
},
never: function() {
return exactly(0);
},
atLeast: function(num) {
_expectedCalls.min = num;
_expectedCalls.max = Nan;
return _self;
},
noMoreThan: function(num) {
_expectedCalls.min = 0;
_expectedCalls.max = num;
return _self;
}
});
this.times = _times;
Next up, let’s make sure we can verify that the expected number of calls have actually been made. We’ll need to go back and update our ‘called’ method to record the incoming calls…
//notifies this setup that it has been called
(_calls = []),
(_called = function(params) {
_calls.push(params);
});
…and then add a new ‘verify’ method to check the count:
//verify that the number of registered calls is within range
_verify = function() {
if (
_calls.length < _expectedCalls.min ||
_calls.length > _expectedCalls.max
) {
//build up a human-readable message...
var expectedCount = _expectedCalls.min;
if (_expectedCalls.max != _expectedCalls.min)
expectedCount =
expectedCount +
'-' +
(_expectedCalls.max === NaN ? '*' : _expectedCalls.max); //...and throw an exception
throw 'Expected ' +
expectedCount +
' calls to ' +
member +
' but had ' +
_calls.length;
}
};
this.verify = _verify;
Callbacks
For the final feature of our Setup, we need to add the ability to register a callback that will be called when the mock method is invoked. The Setup already gets notified through ‘called’ so we just need to add a method that registers the callback, then update ‘called’ to invoke each callback in turn.
//notifies this setup that it has been called
(_calls = []),
(_called = function(params) {
_calls.push(params);
for (var i = 0; i < _callbacks.length; i++) {
_callbacks[i].apply(this, params);
}
}), //store registered callbacks
(_callbacks = []),
(_callback = function(callback) {
_callbacks.push(callback);
});
this.callback = _callback;
We can now specify a callback with a nice human-readable syntax:
mockObject.setup('post').callback(function(url, success) {
success(); //fake a successful post
});
Done
…and that’s pretty much it. It’s not a fully featured mocking library but it it does most of what I would use day-to-day, and it was an interesting project.
I can set up a fake jQuery object expecting a call to ‘post’ with a URL specified, a limit on the number of calls that should be made, a return value and a callback to invoke the success callback parameter:
mockObject
.setup('post')
.with('/expected/url', jsMock.constants.anything)
.times.noMoreThan(1)
.returns(123)
.callback(function(url, success) {
success(mockData);
});
//run test method
mockObject.verify();
The code is available on Github (with some changes from the examples, which have been written for clarity) so help yourselves. I may continue to update it if I actually end up using it!