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.
- You belong to the growing number of Durandal developer?
- You have a need to make your apps better?
- 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
- install node from http://nodejs.org
- install grunt using
npm install -g grunt-cli
- download/clone this repo
- run
npm install
in repo’s root directory to install grunt’s dependencies - 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...
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.