Automate JavaScript Testing with Grunt.js
So far we've learned how to test your JavaScript code with Jasmine and running them against Node.js and browsers with Karma. We've also got familiar with modular design patterns in JavaScript. And yet, somehow it seems that we're still missing one last puzzle piece connecting all the others, it's called Grunt.js.
What is it?
According to it's site:
In one word: automation. The less work you have to do when performing repetitive tasks like minification, compilation, unit testing, linting, etc, the easier your job becomes. After you've configured it, a task runner can do most of that mundane work for you—and your team—with basically zero effort.Zero or not, there is a bit of effort in making everything play together, but no worry - we'll figure it out. So what's our plan?
- Write classes, which are both usable in Node.js, Require.js and global environment.
- Write Jasmine specs to test our code in both Chrome and Firefox
- Write Karma and Node.js runners
- Write Grunt task to automate the testing
Writing universal JavaScript classes
In the end we'll type one command to test our code from every aspect. Feeling excited? Let's start! All the code can be found in GitHub, to where I copied some code from my project called Raceme.js, JavaScript clustering algorithms framework (some harmless PR :) First one is Vector class, which wraps the JavaScript array with minor functionality:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | ( function () { 'use strict' ; var Vector = function Vector(v) { var vector = v; this .length = function length() { return vector.length; }; this .toArray = function toArray() { return vector; }; }; if ( typeof define === 'function' && define.amd) { // Publish as AMD module define( function () { return Vector;}); } else if ( typeof (module) !== 'undefined' && module.exports) { // Publish as node.js module module.exports = Vector; } else { // Publish as global (in browsers) var Raceme = window.Raceme = window.Raceme || {}; Raceme.Common = Raceme.Common || {}; Raceme.Common.Vector = Vector; } }()); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | ( function () { 'use strict' ; var COMMONJS_TYPE = 2, GLOBAL_TYPE = 3; var loadDependencies = function loadDependencies(callback) { if ( typeof define === 'function' && define.amd) { // define AMD module with dependencies define([ 'common/Vector' ], callback); // cannot pass env type } else if ( typeof (module) !== 'undefined' && module.exports) { // load CommonJS module callback(require( '../common/Vector.js' ), COMMONJS_TYPE); } else { // Publish as global (in browsers) callback(Raceme.Common.Vector, GLOBAL_TYPE); } }; loadDependencies( function (Vector, env) { var PlaneMapper = function () { var mapVector = function mapVector(node) { return new Vector([node.x, node.y]); }; return { mapVector: mapVector }; }; // finalize the declaration switch (env) { case COMMONJS_TYPE: module.exports = PlaneMapper(); break ; case GLOBAL_TYPE: var Raceme = window.Raceme = window.Raceme || {}; Raceme.DataMappers = Raceme.DataMappers || {}; Raceme.DataMappers.PlaneMapper = PlaneMapper(); break ; default : return PlaneMapper(); } }); }()); |
Writing universal Jasmine specs
Code is written, time for testing. We'll create two Jasmine specs, each for one of the classes. As in before, we start with Vector class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | ( function () { 'use strict' ; describe( 'Mappers' , function () { var loadDependencies = function loadDependencies(callback) { if ( typeof define === 'function' && define.amd) { // load AMD module define([ 'common/Vector' ], callback); } else if ( typeof (module) !== 'undefined' && module.exports) { // load CommonJS module callback(require( '../../src/common/Vector.js' )); } else { // Publish as global (in browsers) callback(Raceme.Common.Vector); } }; loadDependencies( function (Vector) { var vector; describe( 'Vector' , function () { beforeEach( function () { vector = new Vector([1, 2, 3]); }); it( 'check length' , function () { expect(vector.length()).toEqual(3); }); it( 'check toArray' , function () { expect(vector.toArray()).toEqual([1, 2, 3]); }); }); }); }); })(); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | ( function () { 'use strict' ; describe( 'Mappers' , function () { var loadDependencies = function loadDependencies(callback) { if ( typeof define === 'function' && define.amd) { // load AMD module define([ 'common/Vector' , 'dataMappers/PlaneMapper' ], callback); } else if ( typeof (module) !== 'undefined' && module.exports) { // load CommonJS module callback(require( '../../src/common/Vector.js' ), require( '../../src/dataMappers/PlaneMapper.js' )); } else { // Publish as global (in browsers) callback(Raceme.Common.Vector, Raceme.DataMappers.PlaneMapper); } }; loadDependencies( function (Vector, PlaneMapper) { var vector; describe( 'PlaneMapper' , function () { var mapper, node; beforeEach( function () { mapper = PlaneMapper; node = { x: 5, y: 10 }; }); it( 'check mapping' , function () { vector = mapper.mapVector(node); expect(vector.toArray()).toEqual([5, 10]); }); }); }); }); })(); |
Configuring Jasmine spec runners
Testing Node.js modules is easy - just run the jasmine-node command with path to the specs.
jasmine-node test/specMoving on to browser testing. We'll start with easier case using global declarations. First we create Karma configuration file, karma.conf.js. The main interest is in files and browsers sections, where we define our source and spec files in correct order and browsers we want to test.
1 2 3 4 5 6 7 8 9 | ... files: [ 'src/common/*.js' , 'src/dataMappers/*.js' , 'test/spec/*Spec.js' ], ... browsers: [ 'Chrome' , 'Firefox' ], ... |
karma start karma.conf.jsLastly, let's test our Require.js modules. Since the modules will by loaded by Require.js instead of Karma, a new Karma configuration file is required - karma.conf.require.js. The first difference appears in frameworks section, where we tell Karma to use Require.js framework. This will require installing additional package called karma-requirejs.
1 2 3 4 5 6 7 8 9 10 | ... frameworks: [ 'jasmine' , 'requirejs' ], ... files: [ {pattern: 'src/common/*.js' , included: false }, {pattern: 'src/dataMappers/*.js' , included: false }, {pattern: 'test/spec/*Spec.js' , included: false }, 'test/test-require-main.js' ], ... |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | ( function () { 'use strict' ; var tests = []; for ( var file in window.__karma__.files) { if (window.__karma__.files.hasOwnProperty(file)) { if (/Spec\.js$/.test(file)) { tests.push(file.replace(/^\/base\ //, } } } requirejs.config({ // Karma serves files from '/base' // ask Require.js to load these files (all our tests) deps: tests, // start test run, once Require.js is done callback: window.__karma__.start }); }()); |
Writing Grunt tasks
As promised, it's time to integrate all parts using Grunt.js. For this to happen, we'll require four packages: grunt, grunt-cli and grunt-karma, grunt-jasmine-node. The first two for running the tasks and the rest are for calling Karma and Node.js runners. Make sure to install the packages locally into project's folder, otherwise it will not work. In fact all the packages should be installed locally, when you work with Grunt.js.
Installing them can be done easily using package.json and bower.json files. Once the files are in place just call appropriate install commands. It will download all the packages automatically into project's folder.
npm install bower installIf you an eager environmentalist like me, who doesn't wish to store anything, but essential data on your repository, you may use .gitignore file, which tells Git to ignore specified paths.
node_modules/ bower_components/Grunt tasks are defined using JavaScript code in gruntfile.js.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | ( function () { 'use strict' ; module.exports = function (grunt) { grunt.initConfig({ pkg: grunt.file.readJSON( 'package.json' ), karma: { unit_global: { configFile: 'karma.conf.js' }, unit_requirejs: { configFile: 'karma.conf.require.js' } }, jasmine_node: { options: { forceExit: true , match: '.' , matchall: false , extensions: 'js' , specNameMatcher: 'spec' }, all: [ 'test/spec/' ] } }); grunt.loadNpmTasks( 'grunt-karma' ); grunt.loadNpmTasks( 'grunt-jasmine-node' ); grunt.registerTask( 'default' , [ 'jasmine_node' , 'karma:unit_global' , 'karma:unit_requirejs' ]); }; }()); |
Tasks names can be changed, both jasmine_node and karma node's names cannot.
Aren't you eager to see the results?
gruntGrunt will load and run the gruntfile.js file emitting the following result:
Running "jasmine_node:all" (jasmine_node) task Common Vector check length check toArray Mappers PlaneMapper check mapping Finished in 0.014 seconds 3 tests, 3 assertions, 0 failures Running "karma:unit_global" (karma) task INFO [karma]: Karma v0.12.23 server started at http://localhost:9876/ INFO [launcher]: Starting browser Chrome INFO [launcher]: Starting browser Firefox INFO [Chrome 36.0.1985]: Connected on socket HrOcIkaJ5aqQG85SOqIS with id 63263274 Chrome 36.0.1985: Executed 3 of 3 SUCCESS (0.032 secs / 0.005 secs) INFO [Firefox 31.0.0]: Connected on socket wrrkgK5_skzDJztmOqIT wi Chrome 36.0.1985: Executed 3 of 3 SUCCESS (0.032 secs / 0.005 secs Chrome 36.0.1985: Executed 3 of 3 SUCCESS (0.032 secs / 0.005 secs Chrome 36.0.1985: Executed 3 of 3 SUCCESS (0.032 secs / 0.005 secs Chrome 36.0.1985: Executed 3 of 3 SUCCESS (0.032 secs / 0.005 secs Chrome 36.0.1985: Executed 3 of 3 SUCCESS (0.032 secs / 0.005 secs) Firefox 31.0.0: Executed 3 of 3 SUCCESS (0.026 secs / 0.002 secs) TOTAL: 6 SUCCESS Running "karma:unit_requirejs" (karma) task INFO [karma]: Karma v0.12.23 server started at http://localhost:9876/ INFO [launcher]: Starting browser Chrome INFO [launcher]: Starting browser Firefox INFO [Chrome 36.0.1985]: Connected on socket PXxh9c5vacKQovhSOsI2 with id 36823086 Chrome 36.0.1985: Executed 3 of 3 SUCCESS (0.004 secs / 0.002 secs) INFO [Firefox 31.0.0]: Connected on socket Xu3qldD3wfmNskyOOsI3 wi Chrome 36.0.1985: Executed 3 of 3 SUCCESS (0.004 secs / 0.002 secs Chrome 36.0.1985: Executed 3 of 3 SUCCESS (0.004 secs / 0.002 secs Chrome 36.0.1985: Executed 3 of 3 SUCCESS (0.004 secs / 0.002 secs Chrome 36.0.1985: Executed 3 of 3 SUCCESS (0.004 secs / 0.002 secs Chrome 36.0.1985: Executed 3 of 3 SUCCESS (0.004 secs / 0.002 secs) Firefox 31.0.0: Executed 3 of 3 SUCCESS (0.005 secs / 0.002 secs) TOTAL: 6 SUCCESS Done, without errors.Perfection! But it's only a tip of the iceberg. We'll be talking more about Grunt.js using conditional logic and reporting, so stay tuned ;)
Comments
Post a Comment