Skip to main content

2 posts tagged with "promises"

View All Tags

Unit Testing an Angular Controller with Jasmine

Anyone who reads my blog will know that I have been long in the habit of writing unit tests for my C# code. I'm cool like that. However, it took me a while to get up and running writing unit tests for my JavaScript code. I finally got there using a combination of Jasmine 2.0 and Chutzpah. (Jasmine being my test framework and Chutzpah being my test runner.)

I'm getting properly into the habit of testing my JavaScript. I won't pretend it's been particularly fun but I firmly believe it will end up being useful... That's what I tell myself during the long dark tea-times of the soul anyway.

I have a side project called Proverb. It doesn't do anything in particular - for the most part it's a simple application that displays the collected wise sayings of a team that I used to be part of. There's not much to it - a bit of CRUD, a dashboard. Not much more. Because of the project's simplicity it's ideal to use Proverb's underlying idea when trying out new technologies / frameworks. The best way to learn is to do. So if I want to learn "X", then building Proverb using "X" is a good way to go.

I digress already. I had a version of Proverb built using a combination of AngularJS and TypeScript. I had written the Angular side of Proverb without any tests. Now I was able to write JavaScript tests for my Angular code that's just what I set out to do. It should prove something of a of Code Kata too.

Whilst I'm at it I thought it might prove helpful if I wrote up how I approached writing unit tests for a single Angular controller. So here goes.

What I'm Testing#

I have an Angular controller called sagesDetail. It powers this screen:

sagesDetail is a very simple controller. It does these things:

  1. Load the "sage" (think of it as just a "user") and make it available on the controller so it can be bound to the view.
  2. Set the view title.
  3. Log view activation.
  4. Expose a gotoEdit method which, when called, redirects the user to the edit screen.

The controller is written in TypeScript and looks like this:

sagesDetail.ts#

module controllers {
"use strict";
var controllerId = "sageDetail";
interface sageDetailRouteParams extends ng.route.IRouteParamsService {
id: string;
}
class SageDetail {
log: loggerFunction;
sage: sage;
title: string;
static $inject = ["$location", "$routeParams", "common", "datacontext"];
constructor(
private $location: ng.ILocationService,
private $routeParams: sageDetailRouteParams,
private common: common,
private datacontext: datacontext
) {
this.sage = undefined;
this.title = "Sage Details";
this.log = common.logger.getLogFn(controllerId);
this.activate();
}
// Prototype methods
activate() {
var id = parseInt(this.$routeParams.id, 10);
var dataPromises: ng.IPromise<any>[] = [this.datacontext.sage.getById(id, true).then(data => this.sage = data)];
this.common.activateController(dataPromises, controllerId, this.title)
.then(() => {
this.log("Activated Sage Details View");
this.title = "Sage Details: " + this.sage.name;
});
}
gotoEdit() {
this.$location.path("/sages/edit/" + this.sage.id);
}
}
angular.module("app").controller(controllerId, SageDetail);
}

When compiled to JavaScript it looks like this:

sageDetail.js#

var controllers;
(function (controllers) {
"use strict";
var controllerId = "sageDetail";
var SageDetail = (function () {
function SageDetail($location, $routeParams, common, datacontext) {
this.$location = $location;
this.$routeParams = $routeParams;
this.common = common;
this.datacontext = datacontext;
this.sage = undefined;
this.title = "Sage Details";
this.log = common.logger.getLogFn(controllerId);
this.activate();
}
// Prototype methods
SageDetail.prototype.activate = function () {
var _this = this;
var id = parseInt(this.$routeParams.id, 10);
var dataPromises = [this.datacontext.sage.getById(id, true).then(function (data) {
return _this.sage = data;
})];
this.common.activateController(dataPromises, controllerId, this.title).then(function () {
_this.log("Activated Sage Details View");
_this.title = "Sage Details: " + _this.sage.name;
});
};
SageDetail.prototype.gotoEdit = function () {
this.$location.path("/sages/edit/" + this.sage.id);
};
SageDetail.$inject = ["$location", "$routeParams", "common", "datacontext"];
return SageDetail;
})();
angular.module("app").controller(controllerId, SageDetail);
})(controllers || (controllers = {}));
//# sourceMappingURL=sageDetail.js.map

Now for the Tests#

I haven't yet made the move of switching over my Jasmine tests from JavaScript to TypeScript. (It's on my list but there's only so many things you can do at once...) For that reason the tests you'll see here are straight JavaScript. Below you will see the tests for the sageDetail controller.

I have put very comments in the test code to make clear the intent to you, dear reader. Annotated the life out of them. Naturally I wouldn't expect a test to be so heavily annotated in a typical test suite - and you can be sure mine normally aren't!

Jasmine tests for sageDetail.js#

describe("Proverb.Web -> app-> controllers ->", function () {
// Before each test runs we're going to need ourselves an Angular App to test - go fetch!
beforeEach(function () {
module("app"); // module is an alias for <a href="https://docs.angularjs.org/api/ngMock/function/angular.mock.module">angular.mock.module</a>
});
// Tests for the sageDetail controller
describe("sageDetail ->", function () {
// Declare describe-scoped variables
var $rootScope,
getById_deferred, // deferred used for promises
$location, $routeParams_stub, common, datacontext, // controller dependencies
sageDetailController; // the controller
// Before each test runs set up the controller using inject - an alias for <a href="https://docs.angularjs.org/api/ngMock/function/angular.mock.inject">angular.mock.inject</a>
beforeEach(inject(function (_$controller_, _$rootScope_, _$q_, _$location_, _common_, _datacontext_) {
// Note how each parameter is prefixed and suffixed with "_" - this an Angular nicety
// which allows you to have variables in your tests with the original reference name.
// So here we assign the injected parameters to the describe-scoped variables:
$rootScope = _$rootScope_;
$q = _$q_;
$location = _$location_;
common = _common_;
datacontext = _datacontext_;
// Our controller has a dependency on an "id" property passed on the $routeParams
// We're going to stub this out with a JavaScript object literal
$routeParams_stub = { id: "10" };
// Our controller depends on a promise returned from this function: datacontext.sage.getById
// Well strictly speaking it also uses a promise for activateController but since the activateController
// promise just wraps the getById promise it will be resolved when the getById promise is.
// Here we create a deferred representing the getById promise which we can resolve as we need to
getById_deferred = $q.defer();
// set up a spy on datacontext.sage.getById and set it to return the promise of getById_deferred
// this allows us to #1 detect that getById has been called
// and #2 resolve / reject our promise as our test requires using getById_deferred
spyOn(datacontext.sage, "getById").and.returnValue(getById_deferred.promise);
// set up a spy on common.activateController and set it to call through
// this allows us to detect that activateController has been called whilst
// maintaining existing controller functionality
spyOn(common, "activateController").and.callThrough();
// set up spys on common.logger.getLogFn and $location.path so we can detect they have been called
spyOn(common.logger, "getLogFn").and.returnValue(jasmine.createSpy("log"));
spyOn($location, "path").and.returnValue(jasmine.createSpy("path"));
// create a sageDetail controller and inject the dependencies we have set up
sageDetailController = _$controller_("sageDetail", {
$location: $location,
$routeParams: $routeParams_stub,
common: common,
datacontext: datacontext
});
}));
// Tests for the controller state at the point of the sageDetail controller's creation
// ie before the getById / activateController promises have been resolved
// So this tests the constructor (function) and the activate function up to the point
// of the promise calls
describe("on creation ->", function () {
it("controller should have a title of 'Sage Details'", function () {
// tests this code has executed:
// this.title = "Sage Details";
expect(sageDetailController.title).toBe("Sage Details");
});
it("controller should have no sage", function () {
// tests this code has executed:
// this.sage = undefined;
expect(sageDetailController.sage).toBeUndefined();
});
it("datacontext.sage.getById should be called", function () {
// tests this code has executed:
// this.datacontext.sage.getById(id, true)
expect(datacontext.sage.getById).toHaveBeenCalledWith(10, true);
});
});
// Tests for the controller state at the point of the resolution of the getById promise
// ie after the getById / activateController promises have been resolved
// So this tests the constructor (function) and the activate function after the point
// of the promise calls
describe("activateController ->", function () {
var sage_stub;
beforeEach(function () {
// Create a sage stub which will be used when resolving the getById promise
sage_stub = { name: "John" };
});
it("should set sages to be the resolved promise values", function () {
// Resolve the getById promise with the sage stub
getById_deferred.resolve(sage_stub);
$rootScope.$digest(); // So Angular processes the resolved promise
// tests this code has executed:
// this.sage = data
expect(sageDetailController.sage).toBe(sage_stub);
});
it("should log 'Activated Sage Details View' and set title with name", function () {
// Resolve the getById promise with the sage stub
getById_deferred.resolve(sage_stub);
$rootScope.$digest(); // So Angular processes the resolved promise
// tests this code has executed:
// this.log("Activated Sage Details View");
// this.title = "Sage Details: " + this.sage.name;
expect(sageDetailController.log).toHaveBeenCalledWith("Activated Sage Details View");
expect(sageDetailController.title).toBe("Sage Details: " + sage_stub.name);
});
});
// Tests for the gotoEdit function on the controller
// Note that this will only be called *after* a controller has been created
// and it depends upon a sage having first been loaded
describe("gotoEdit ->", function () {
var sage_stub;
beforeEach(function () {
// Create a sage stub which will be used when resolving the getById promise
sage_stub = { id: 20 };
});
it("should set $location.path to edit URL", function () {
// Resolve the getById promise with the sage stub
getById_deferred.resolve(sage_stub);
$rootScope.$digest(); // So Angular processes the resolved promise
sageDetailController.gotoEdit();
// tests this code has executed:
// this.$location.path("/sages/edit/" + this.sage.id);
expect($location.path).toHaveBeenCalledWith("/sages/edit/" + sage_stub.id);
});
});
});
});

Rolling your own confirm mechanism using Promises and jQuery UI

It is said that a picture speaks a thousand words. So here's two:

That's right, we're here to talk about the confirm dialog. Or, more specifically, how we can make our own confirm dialog.

JavaScript in the browser has had the window.confirm method for the longest time. This method takes a string as an argument and displays it in the form of a dialog, giving the user the option to click on either an "OK" or a "Cancel" button. If the user clicks "OK" the method returns true, if the user clicks "Cancel" the method returns false.

window.confirm is wonderful in one way - it has a simple API which is easy to grok. But regardless of the browser, window.confirm is always as ugly as sin. Look at the first picture in this blog post; hideous. Or, put more dispassionately, it's not terribly configurable; want to change the button text? You can't. Want to change the styling of the dialog? You can't. You get the picture.

Making confirm 2.0#

jQuery UI's dialog has been around for a long time. I've been using it for a long time. But, if you look at the API, you'll see it works in a very different way to window.confirm - basically it's all about the callbacks. My intention was to create a mechanism which allowed me to prompt the user with jQuery UI's tried and tested dialog, but to expose it in a way that embraced the simplicity of the window.confirm API.

How to do this? Promises! To quote Martin Fowler (makes you look smart when you do that):

"In Javascript, promises are objects which represent the pending result of an asynchronous operation. You can use these to schedule further activity after the asynchronous operation has completed by supplying a callback."

When we show our dialog we are in asynchronous land; waiting for the user to click "OK" or "Cancel". When they do, we need to act on their response. So if our custom confirm dialog returns a promise of a boolean (true when the users click "OK", false otherwise) then that should be exactly what we need. I'm going to use Q for promises. (Nothing particularly special about Q - it's one of many Promises / A+ compliant implementations available.)

Here's my custom confirm dialog:

/**
* Show a "confirm" dialog to the user (using jQuery UI's dialog)
*
* @param {string} message The message to display to the user
* @param {string} okButtonText OPTIONAL - The OK button text, defaults to "Yes"
* @param {string} cancelButtonText OPTIONAL - The Cancel button text, defaults to "No"
* @param {string} title OPTIONAL - The title of the dialog box, defaults to "Confirm..."
* @returns {Q.Promise<boolean>} A promise of a boolean value
*/
function confirmDialog(message, okButtonText, cancelButtonText, title) {
okButtonText = okButtonText || "Yes";
cancelButtonText = cancelButtonText || "No";
title = title || "Confirm...";
var deferred = Q.defer();
$('<div title="' + title + '">' + message + '</div>').dialog({
modal: true,
buttons: [{
// The OK button
text: okButtonText,
click: function () {
// Resolve the promise as true indicating the user clicked "OK"
deferred.resolve(true);
$(this).dialog("close");
}
}, {
// The Cancel button
text: cancelButtonText,
click: function () {
$(this).dialog("close");
}
}],
close: function (event, ui) {
// Destroy the jQuery UI dialog and remove it from the DOM
$(this).dialog("destroy").remove();
// If the promise has not yet been resolved (eg the user clicked the close icon)
// then resolve the promise as false indicating the user did *not* click "OK"
if (deferred.promise.isPending()) {
deferred.resolve(false);
}
}
});
return deferred.promise;
}

What's happening here? Well first of all, if okButtonText, cancelButtonText or title have false-y values then they are initialised to defaults. Next, we create a deferred object with Q. Then we create our modal dialog using jQuery UI. There's a few things worth noting about this:

  • We're not dependent on the dialog markup being in our HTML from the off. We create a brand new element which gets added to the DOM when the dialog is created. (I draw attention to this as the jQuery UI dialog documentation doesn't mention that you can use this approach - and frankly I prefer it.)
  • The "OK" and "Cancel" buttons are initialised with the string values stored in okButtonText and cancelButtonText. So by default, "Yes" and "No".
  • If the user clicks the "OK" button then the promise is resolved with a value of true.
  • If the dialog closes and the promise has not been resolved then the promise is resolved with a value of false. This covers people clicking on the "Cancel" button as well as closing the dialog through other means.

Finally we return the promise from our deferred object.

Going from window.confirm to confirmDialog#

It's very simple to move from using window.confirm to confirmDialog. Take this example:

if (window.confirm("Are you sure?")) {
// Do something
}

Becomes:

confirmDialog("Are you sure?").then(function(confirmed) {
if (confirmed) {
// Do something
}
});

There's no more to it than that.

And finally a demo...#

With the JSFiddle below you can create your own custom dialogs and see the result of clicking on either the "OK" or "Cancel" buttons.