Tuesday, July 23, 2013

A Grunting Build

This post is a follow-up on the Dojo and Jasmine entry where we explain how to use the Jasmine framework to unit test a Dojo based application.

In this post, we're going to run our unit tests automatically when the application is built (why the heck would we need to build a javascript app?).

Grunt is described as a JavaScript task runner. It's slowly becoming a defacto-standard in the JavaScript world and it constitutes the central piece of the popular yeoman suite of tools.

To make an analogy with Java build tools, grunt is closer to ant in spirit than maven.

Tasks are defined using a plugin mechanism and an already impressive list is available directly on the site which confirms the popularity of the project. 

Grunt is a node.js  module, so make sure it is setup on your system (remember versions with an even minor number are consider stable, I have 0.10.12). 

Here after my grunt script:

module.exports = function (grunt) {
    var distDir = 'dist/',httpServerPort=9001,httpServerPortTest=9002;
    /*build directory*/
    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),
        //clean up destination dir
        clean: {
            clean: [distDir]
        },
        //minimize javascript modules
        uglify: {
            options: {
                // the banner is inserted at the top of the output
                banner: '/*! <%= pkg.name %> - v<%= pkg.version %> - ' +
                    '<%= grunt.template.today("yyyy-mm-dd") %> */',
                report: 'min'
            },
            dist: {
                files: [
                    {
                        expand: true,     // Enable dynamic expansion.
                        //cwd: '/',      // Src matches are relative to this path.
                        src: ['js/com/**/*.js'], // Actual pattern(s) to match.
                        dest: distDir   // Destination path prefix.
                        /*ext: '.min.js',   // We don't do that on Dojo module because name is important*/
                    }
                ]
            }
        },
        //minimize stylesheets
        cssmin: {
            options: {
                // the banner is inserted at the top of the output
                banner: '/*! <%= pkg.name %> - v<%= pkg.version %> - <%= grunt.template.today("yyyy-mm-dd") %> */',
                report: 'min'
            },
            dist: {
                files: [
                    {
                        expand: true,     // Enable dynamic expansion.
                        cwd: '',      // Src matches are relative to this path.
                        src: ['css/**/*.css'], // Actual pattern(s) to match.
                        dest: distDir   // Destination path prefix.
                    }
                ]
            }
        },
        //static analysis of js modules
        jshint: {
            // configure JSHint (documented at http://www.jshint.com/docs/)
            options: {
                // more options here if you want to override JSHint defaults
                globals: {
                    console: true,
                    module: true
                }
            },
            // define the files to lint
            files: ['js/com/**/*.js', 'test/com/**/*.js']
        },
        //execute a shell command (in that case phantomjs for headless testing)
        exec: {
            //if a local http server is present
            'test-local': {
                command: 'phantomjs test/run-jasmine.js http://localhost:'+httpServerPortTest+'/test/SpecRunner.html'
            },
            //using test machine
            'test-remote': {
                    command: 'phantomjs test/run-jasmine.js http://user:password@192.168.1.100/test/SpecRunner.html'
            }
        },

        copy: {
            main: {
                files: [
                    {expand: true, src: ['text/**', 'img/**', 'js/*.min.js', '*.ico'], dest: distDir}
                ]
            },
            js: {
                files: [
                    {expand: true, src: ['js/**/*.js', '*.ico'], dest: distDir}
                ]
            }
        },
        processhtml: {
            options: {
                data: {
                    version: '<%= pkg.version %>'
                }
            },
            files: {expand: true, src: ['*.html'], dest: distDir}
        },
        connect: {
            options: {
                hostname: "localhost",
                base: "."
            },
            server: {
                options: {
                    port: httpServerPort
                }
            },
            test: {
                options: {
                    port: httpServerPortTest
                }
            }
        }
    });

    // load all grunt tasks
    require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks);

    // the default task can be run just by typing "grunt" on the command line
    grunt.registerTask('build', ['clean', 'jshint', 'connect:test', 'exec:test-local', 'cssmin', 'copy', 'processhtml']);
    //running grunt dev will run the unit test on the test machine
    grunt.registerTask('remote', ['clean', 'jshint', 'exec:test-remote', 'cssmin', 'copy', 'processhtml']);
    //run this to start up a local http server on current directory
    grunt.registerTask('dev', ['connect:server:keepalive'])

    grunt.registerTask('default', function () {
        var blue = '\033[34m', reset = '\033[0m', green = '\033[32m', cyan = '\033[36m';
        console.log("\n\n");
        console.log(blue + "Welcome to the sample-app grunt build file\n");
        console.log(reset + "The following grunt tasks are available:");
        console.log(reset + "****************************************\n");
        console.log(green + "build:\t\t"+ cyan + "This task builds the entire application. Directory "  + distDir + " will be available for distribution\n");
        console.log(green + "remote:\t\t"+ cyan + "Same thing as build except that tests will be executed remotely against the TEST server. Directory "  + distDir + " will still be available for distribution\n");
        console.log(green + "dev:\t\t"+ cyan + "This will start a local http server listening on port " + httpServerPort + ". This is very handy at development time.");
        console.log("\n\n");
        console.log(reset + "Example:");
        console.log(reset + "********\n");
        console.log(blue + "grunt build");

    });
};

In the first part of the code, we configure the different plugins we're using in our build.

At the end we register some customs coarse grain tasks that simply run some of the tasks we configured earlier.

The default task at the very end will run if no specific argument is given to the grunt command. It simply displays a help instructing the user on how to use the build.

For example running 'grunt build' on the command line cleans the distribution directory, jshint (ref) your js files, starts a local http server, executes unit tests, minifies the style sheets, copies different files and finally processes the html to insert the app version number and replaces development time js to their minified counterpart.

If any of the individual tasks fails, the build is stopped with an error exit code. In order to run our tests, we use the grunt-exec plugin which simply executes a shell command.

PhantomJS is a headless browser (i.e. without a user interface) that is fetching a page from a given url (our Jasmine test runner in this case) and process it just like a regular browser.

PhantomJS is using the Webkit rendering engine also used in Google Chrome (up to version 27) and Safari.

Once the page is loaded we need to instruct PhantomJS what to do with it. In our case we want to know if the Jasmine Test Runner ends up successfully or not. We'd also like to capture the individual tests result and display them in the shell console. The `run-jasmine.js` argument passed on the command line does just that.  This script is available as an example on the PhantomJS site.

Just remember that PhantomJS is not a node module. You will need to setup it up on your build server.

PhantomJS is very handy to run our unit tests but how confident are we that our application will behave correctly with different browsers (IE, FF, Opera)?

That might be the subject of another post. Stay tuned.


No comments:

Post a Comment

Thank you for your comment