Running an Express server with Grunt and Yeoman: Part 3
Back in part 1 of this series I introduced a small number of tools that make a web engineer’s life easier: Grunt, Yeoman, Express, and Nodemon. In part 2 I provided a repository of files and explained how to use them to have a running server that’s capable of automatic browser and app server reboots for rapid development.
Now, in part 3, I’ll explain how we can use this workflow to automatically run unit tests when development files or test files change, backend or frontend, and how to expose internal functions and properties in our Node or RequireJS modules to the testing environment so that we get less bugs. Onward!
The Skinny
Our repository has been outfitted with Karma, Mocha, Chai, Sinon, Sinon-Chai, and Istanbul. In our starter set of files in the repository, a neat set of examples have been written to familiarize you with how they can fit together in your project. In addition, Grunt tasks have been included to leverage these tools to run your unit tests and code coverage whenever your files change.
You don’t need to do anything more than create test files in the test/
directory that end in .spec.js
, create client-side files in app/scripts/app/
, and server-side files in lib/
. In the coverage/
directory you’ll find .html
files showing just how thorough and robust your tests really are. This’ll give you a greater level of confidence in your work now and in the future.
Special Notes
There’s an issue in the The grunt-contrib-watch
task where if two targets are watching the same file(s), only the last defined target will run if the file(s) change. Right now this means client-side tests will run and livereload will not when client-side scripts are saved. You can switch this behavior by altering the watch target order in Gruntfile.js
. Watch grunt-contrib-watch issue #25 and upgrade your local copy of that library using npm
when it’s fixed.grunt-contrib-watch
task has been updated and grunt-express-workflow
has been updated to include the fix! Thanks to the grunt-contrib-watch
developers for a fantastic tool.
Since your RequireJS client-side development environment, grunt build
environment, and test environment are by necessity all different, it’s important that you maintain each environment’s corresponding RequireJS configuration file. These are found in app/scripts/app/config.js
, Gruntfile.js
under the requirejs
task, and test/frontend/app/config.js
, respectively.
You can configure the grunt-karma
task to use multiple browsers for testing. Adding 'PhantomJS'
as a test environment gave me issues, but they turned out to be that my version of node was outdated and Chai has an issue with should
on PhantomJS. These can be fixed by running Node >= v0.10.12
and with the shim I included in test/frontend/app/config.js
.
Running Tests Manually
The grunt test
command is configured to run your frontend and backend tasks with code coverage. This can be changed easily to no coverage in the Gruntfile
. The grunt build
command is configured to run these tests and output a production build of your application if they pass.
You can run tests for either your frontend or backend files right from the command-line. If you’d like to run your backend tests with coverage, use grunt backendCoverage
; without coverage, use grunt simplemocha:backend
. This command is significantly faster.
For the frontend, the command for running tests with coverage is grunt karma:appContinuous
. “Continuous” signifies that the test run is also a single-run, not a long-running process. For a test run without coverage, use grunt karma:appContinuousNoCoverage
. The karma:app
and karma:appNoCoverage
tasks are long-running processes that are meant to be run alongside the watch
task if desired. The karma:app
task is configured this way by default, with karma:app:run
declared under the watch
task, taking advantage of the Karma server started by karma:app
in grunt server
.
By modifying the watch
, grunt server
, grunt build
, and grunt test
tasks in the Gruntfile
, you can alter what types of tests run and how.
What is Istanbul and what is code coverage?
Istanbul is a fairly new tool that will give you insight into how well your test cases are covering your production code. It “instruments” your files and when the tests are run, records every time a function, statement, branch, etc. is executed. You’ll know where you may need to aim future tests. Here’s an example of what an Istanbul report looks like.
Running Server-side Node.js Code in a test
Environment
Server-side tests run with the global node.js variable process.env.NODE_ENV
set to 'test'
. You can take advantage of this in your server-side code by exporting more variables or functions for testing when a test for that is true, like so:
function externalFunction (value) {}
function internalFunction (value) {
return value + 2;
}
// Module Exports
exports.externalFunction = externalFunction;
// this allows you to test the internalFunction in your unit tests
if (process.env.NODE_ENV === 'test') {
exports.internalFunction = internalFunction;
}
Exposing Internal Functions and Properties in RequireJS Modules to your Tests
Exposing internal functions and variables in RequireJS apps to your test environment is a little more complicated. Since RequireJS modules export their returned value whenever they’re require()d, we’ll need some way to export something different when they’re require()d in a different context. Has.js to the rescue!
By including has.js in your development environment and including a has
property in your Gruntfile.js
for the requirejs
build task, all you have to do is register a global environment variable with has.add()
and a has()
test in your module, and when your module is require()d, it can potentially return something completely different than it normally would. Anything you want. When you run grunt build
, that entire has()
branch in your module is removed, leaving only your production code. Let’s review a few files to get a better picture of what’s going on.
// views/index.jade
/* ... */
block append bottom-script
- if (dev)
//- has.js is used to branch requirejs modules' returned values in a test environment like mocha.
since the modules themselves contain has() tests, we need to include them in the dev environment.
during a build, r.js will optimize out the has() tests, so has.js is not needed in production.
script(src='/components/has/has.js')
script(src='/components/requirejs/require.js', data-main='/scripts/app/config')
- else
script(src='/scripts/app/main.js')
// app/scripts/app/someModule.js
define([
],
function () {
'use strict';
// a function to illustrate how to test
// the internals of RequireJS modules
function internalFunction (data) {
if (data) {
return 1;
} else {
return 0;
}
}
// a function that's normally returned
// when this module is require()d
function externalFunction () {
return true;
}
// expose anything you want to the test environment.
// you can even choose to return the module's normal value by using `has` within a test suite.
// this entire branch will be removed automatically during a production build.
if (has('internalTest')) {
return {
externalFunction: externalFunction,
internalFunction: internalFunction,
};
}
// otherwise return the module's normal value
return externalFunction;
});
// Gruntfile.js
/* ... */
requirejs: {
/* ... */
app: {
/* ... */
has: {
// a has() assignment only for unit testing. RequireJS modules are
// able to return different values when unit tests set
// this variable to true
internalTest: false,
}
}
},
/* ... */
// test/frontend/app/someModule.spec.js
define([
// this will give us the normal return value for the module
'someModule'
],
function(someModule) {
/*
* External Module Tests:
* tests using only the normal return value from the module itself
*/
describe('someModule external tests', function() {
it('should return true', function() {
expect(someModule).to.be.a('function');
expect(someModule()).to.equal(true);
});
});
/*
* Internal Module Tests:
* tests using the internal functions and properties of the module
*/
describe('someModule internal tests', function () {
// placeholder variable for when we require() the module in a different context
var someModule;
// this is run once, before any tests in this suite take place
before(function (done) {
// set internalTest environment so that the
// someModule module returns internal functions for testing
has.add('internalTest', function () {
return true;
}, true);
// remove the module from the requirejs cache so that we
// can require() it again with the internalTest environment set
require.undef('someModule');
require(['someModule'], function (module) {
someModule = module;
done();
});
});
it('should return an object', function() {
expect(someModule).to.be.an('object');
});
it('should test internalFunction with no args', function () {
expect(someModule).to.have.property('internalFunction');
var result = someModule.internalFunction();
expect(result).to.equal(0);
});
it('should test internalFunction with one argument', function () {
expect(someModule).to.have.property('internalFunction');
var result = someModule.internalFunction({ test: 'data'});
expect(result).to.equal(1);
});
});
});
And that’s how we can make our RequireJS tests a lot more robust!
Expanding to More Namespaced Client-Side Apps
The folder structure and Gruntfile.js
are organized in such a way that the main client-side app lives in app/scripts/app/
. This allows us to make other client-side apps that use RequireJS but that don’t interact with and are separate from the app that lives in app/scripts/app/
. For instance, if you have a single-page app with its own modules and want to create another single-page app for another part of your site with its own modules, it’s possible to do so by creating another namespace for it in, say, app/scripts/otherApp/
. Now all you need to do is set it up with Grunt so that it’ll watch files for changes and run your tests. You’ll need to:
- copy the
karma.app.conf.js
file to a separatekarma.otherApp.conf.js
file - make a
karma:otherApp
target in the Gruntfile’skarma
task and include it in thegrunt server
task - reference the
karma.otherApp.conf.js
file in theconfigFile
property of yourkarma
task target in the Gruntfile - make a
karma:otherAppContinuous
target in thekarma
task and include it in thegrunt build
task - include a
karma:otherApp:run
into thetasks
array of thekarma
target of thewatch
task, or modify thewatch
task to run tests andlivereload
on yourkarma:otherApp:run
task only when its files change
It sounds complicated, but once you get started it’ll make a lot more sense, for sure.
Happy hacking! :)
Comments