Promises are a nice solution to write readable asynchronous code in JavaScript. If you understand Spanish, I highly recommend this great talk on Promises by Enrique Amodeo.
But, is the code with promises easy to unit test? It’s not very difficult, you have to take into account the calls are asynchronous even if the production code executes instantly.
Here is some code I’ve written using when.js library:
function TicketSalesService(){
var self = this;
// ... some other code over here...
this.buyTickets = function(quantity) {
purchaseOrder.quantity = quantity;
// THIS IS THE CODE WITH PROMISES:
this.server.isUserRegistered(user)
.then(tryToBuyTickets)
.then(informSubscribersAboutPurchaseResult)
.otherwise(function (err) {
helpers.registerPromiseError(self, err);
});
};
var tryToBuyTickets = function (response) {
if (!response.registered){
self.onRegistrationRequired(); // trigger event
throw helpers.endOfPromiseChain; // stop the chain
}
return self.server.buyTickets(purchaseOrder);
};
var informSubscribersAboutPurchaseResult = function (response) {
if (response.success)
self.onPurchaseSuccess();
else
self.onPurchaseFailure(response.message);
};
this.onRegistrationRequired = function() {/* event */};
this.onPurchaseSuccess = function() {/* event */};
this.onPurchaseFailure = function() {/* event */};
// ... some other code over here....
};
In this code there is a helper namespace providing a function and a constant:
helpers.endOfPromiseChain; // this is just a constant, a string.
// And this is a function to ease testing:
helpers.registerPromiseError = function (target, err) {
if (err != helpers.endOfPromiseChain){
target.errorInPromise = err.toString();
}
};
The “server” dependency in the TicketSalesService is just a jQuery ajax wrapper. There is a rule in TDD that says, “do not mock artifacts you don’t own”. What I do is wrap up jQuery.ajax so that I can easily stub out the server response and also change jQuery for other library if I needed to.
Service.prototype.isUserRegistered = function (user) {
var deferred = when.defer();
// wrapping jQuery aja:
this.requestData("theUrl/goes/over/here",
{ data: user },
function(data) { // jQuery.ajax success invokes this
deferred.resolve(data);
});
// TODO. call deferred.reject(); on error
return deferred.promise;
};
What about the tests? I am using Jasmine as the testing framework. I test-drove the code above and these are the resulting tests:
it("triggers event when server responds that the user is not registered",
function () {
stubServerResponse(salesService.server, { registered: false });
var promiseSpy = spyReturningPromise(salesService,
"onRegistrationRequired");
salesService.buyTickets(5);
assertAsyncExpects(promiseSpy, salesService);
});
it("tries to buy when server responds that the user is registered",
function () {
stubServerResponse(salesService.server, { registered: true });
var promiseSpy = spyReturningPromise(salesService.server,
"buyTickets");
salesService.buyTickets(5);
assertAsyncExpects(promiseSpy, salesService);
});
it("triggers event when tickets are purchased",
function () {
stubServerResponse(salesService.server,
{success: true, registered: true});
var promiseSpy = spyReturningPromise(salesService,
"onPurchaseSuccess");
salesService.buyTickets(5);
assertAsyncExpects(promiseSpy, salesService);
});
it("triggers event when prescriptions could not be purchased",
function () {
stubServerResponse(salesService.server,
{success: false, registered: true, message: 'fatal'});
var promiseSpy = spyReturningPromise(salesService,
"onPurchaseFailure");
salesService.buyTickets(5);
assertAsyncExpects(promiseSpy, salesService);
});
My code is using DOM Level 0 event handling. You can read more about event driven design in this former post.
The tests are very clean if you are familiar with Jasmine spies.
The method “stubServerResponse” replaces the function “requestData” in my “server” object to simulate data coming from the server.
The other helpers are here:
var assertAsyncExpects =
function(promiseSpy, target, additionalExpectation) {
waitsFor(function () {
return promiseSpy.called ||
target.errorInPromise; }, 50
);
runs(function () {
// this tells me if there was any unhandled exception:
expect(target.errorInPromise).not.toBeDefined();
// this asks the spy if everything was as expected:
expect(promiseSpy.target[promiseSpy.methodName]
).toHaveBeenCalled();
// optional expectations:
if (additionalExpectation)
additionalExpectation();
});
};
var spyReturningPromise =
function(target, methodName) {
var spyObj = {called: false,
target: target,
methodName: methodName};
spyOn(target, methodName).andCallFake(function () {
spyObj.called = true;
return when.defer().promise;
});
return spyObj;
}
These two are basically wrappers around Jasmine to remove noise from my tests. The funcionts “waitsFor”, “runs” and “spyOn”, belong to Jasmine. The first two deal with asynchronous execution whereas the third one creates a test double, a spy object.
What are the tricky parts?
When there is an exception inside the “then” or “otherwise” functions, it is captured by the promise and the test doesn’t know anything about it. So when the test fails, it might not be for an obvious reason, and I want my unit tests to tell me where the problem is very fast. So I create a property in the object containing the “then” methods, called “errorInPromise” that I can check later in my tests. I add the “otherwise” handler at the end of the “then” blocks to ensure any exception thrown within them is captured and can be read in the tests.
What do you do to unit test your “promising code”?