Testing Durandal apps made easy

Hi there,

The today’s post is targeted to a very dedicated audience, so let’s see if the following is for you.

  1. You belong to the growing number of Durandal developer?
  2. You have a need to make your apps better?
  3. You don’t know yet how to test a Durandal app?

If you answered one or multiple of these questions with a yes, read on.

Hey, even you answered the last two questions with a no read on to see if Grunt can make your life easier. When you haven’t heard of Grunt yet, head over to smashingmagazine for a nice Grunt intro.

Prerequisites

Clone the following repo https://github.com/RainerAtSpirit/HTMLStarterKitPro, which is the Grunt enabled version of the Durandal “HTML Starter kit” (at the time of this writing Durandal 2.0.1). Follow the steps in the readme, obviously skipping things you’ve already installed on your machine.

Quick start

  1. install node from http://nodejs.org
  2. install grunt using npm install -g grunt-cli
  3. download/clone this repo
  4. run npm install in repo’s root directory to install grunt’s dependencies
  5. run grunt to run the default task, which opens the resultant _specrunner.html in the browser… and waits for you to write some tests

When everything runs smoothly you should see something like the following while running grunt.

 1     $ grunt
 2     Running "jshint:all" (jshint) task
 3     >> 8 files lint free.
 4 
 5     Running "jasmine:dev" (jasmine) task
 6     Testing jasmine specs via phantom
 7     ..............
 8     14 specs in 0.113s.
 9     >> 0 failures
10 
11     Running "connect:dev:livereload" (connect) task
12     Started connect web server on 127.0.0.1:8999.
13 
14     Running "open:dev" (open) task
15 
16     Running "watch:dev" (watch) task
17     Waiting...

Specrunner

Note: If you are seeing an error message like following instead…

1     $ grunt
2     Loading "Gruntfile.js" tasks...ERROR
3     >> Error: Cannot find module 'connect-livereload'
4     Warning: Task "default" not found. Use --force to continue.
5 Aborted due to warnings.

then reread the instructions, you probably missed step 4 ;-).

Ready for your first test? Time for some myth busting…

Durandal’s AMD modules are hard to grok

Hmh, not really. From a Durandal perspective an AMD module should either return a singleton or a constructor function. Here are two simple examples:

singleton.js

1 define(function () {
2     'use strict';
3 
4     return {
5        type: 'I\'m a singleton'
6     };
7 });

constructor.js

1 define(function () {
2     'use strict';
3 
4     return function(){
5        this.type =  'I\'m a constructor function';
6     };
7 });

On a side note When a module has dependencies than there are two ways to declare them.

singleton.js regular syntax

1 define(['knockout'], function (ko) {
2     'use strict';
3 
4     return {
5        type: 'I\'m a singleton',
6        observable: ko.observable('')
7     };
8 });

singleton.js sugar syntax

1 define(function (require) {
2     'use strict';
3     var ko = require('knockout');
4 
5     return {
6        type: 'I\'m a singleton',
7        observable: ko.observable('')
8     };
9 });

You’ll find both in the wild and it’s a question of personal style, which one to use. Before you ask, I tend to use the sugar syntax lately, but now back to the track.

When I say Durandal’s perspective I mean that the module gets loaded by using system.resolveObject, where Durandal differentiate between modules that return a function and … the rest. Of course Durandal wouldn’t be Durandal if this couldn’t be customized, but that’s another story and you have to read it on your own (see customizing system).

1 resolveObject: function(module) {
2     if (system.isFunction(module)) {
3         return new module();
4     } else {
5         return module;
6     }
7 },

Ok so how can we use our testing environment to figure out what is returned by our modules. Let’s face it just because I assume that one returns a constructor and the other a singleton doesn’t necessarily mean that it’s true.

Start by copying flickr.spec.js to singleton.spec.js and update the spec to the following.

 1 /*global jasmine, describe, beforeEach, it, expect, require */
 2 describe('viewmodels/singleton', function() {
 3     "use strict";
 4 
 5     var singleton = require('viewmodels/singleton');
 6 
 7     it('should have a "type" property', function() {
 8         expect(singleton.type).toBeDefined();
 9     });
10 
11 });

On save you’ll see the grunt watch task kicking in, running the specs updating the browser via livereload. Hopefully all is green, so let’s move on.

Copy singleton.spec.js to constructor.spec.js and update the spec so that it loads constructor.js instead:

 1 /*global jasmine, describe, beforeEach, it, expect, require */
 2 describe('viewmodels/constructor', function() {
 3     "use strict";
 4 
 5     var constructor = require('viewmodels/constructor');
 6 
 7     it('should have a "type" property', function() {
 8         expect(constructor.type).toBeDefined();
 9     });
10 
11 });

This time you should see a nice red warning message at the command prompt telling you that’s something wrong.

 1 >> File "test\specs\dev\constructor.spec.js" changed.
 2 
 3 Running "jasmine:dev" (jasmine) task
 4 Testing jasmine specs via phantom
 5 x...............
 6 viewmodels/constructor:: should have a "type" property: failed
 7   Expected undefined to be defined. (1)
 8 16 specs in 0.011s.
 9 >> 1 failures
10 Warning: Task "jasmine:dev" failed. Use --force to continue.
11 
12 Aborted due to warnings.

Now having a browser that allows us investigating what’s going on becomes pretty handy. Luckily in our case we don’t even need that. We already know that constructor.js returns a constructor function and not an object, so let’s rewrite the test to take that into account, which should bring us back to “all green”.

 1 describe('viewmodels/constructor', function() {
 2     "use strict";
 3 
 4     var Constructor = require('viewmodels/constructor'),
 5         instance = new Constructor();
 6 
 7     it('should be a Constructor function', function() {
 8           var a = new Constructor();
 9           expect(a.constructor).toEqual(Constructor);
10       });
11 
12     it('should have a "type" property', function() {
13         expect(instance.type).toBeDefined();
14     });
15 
16 });

That’s it for today, pretty straight forward, so it can be easily applied/adapted to your own Durandal projects. It’s the first post in the “Catch fish” category, hopefully more to come.

Give a man a fish, and you feed him for a day; show him how to catch fish, and you feed him for a lifetime.

Let me know what kind of fish you can come up with.

Published: November 11 2013

blog comments powered by Disqus