Improving the quality of
your Javascript application

Stephan Hochdörfer // 27.06.2015

About me


  • Stephan Hochdörfer
  • Head of Technology, bitExpert AG
    (Mannheim, Germany)
  • S.Hochdoerfer@bitExpert.de
  • @shochdoerfer

  • #PHP, #DevOps, #Automation, #unKonf

Our story: Diversity

Back in the old days...

Goal: Improve the situation

Common code style

Run tools the "same" way

Hide the complexitiy

Project layout

/tmp
|-myproject
|---src
|---srcweb
|-----js
|-------src
|-------tests
|---------fixture
|---------spec
|-----scss
|-------etc
|-------src
|-------var
|---tests
|---vendor
|---webroot
|-----js
|-----css

package.json

{
    "name": "myproject",
    "version": "0.0.1",
    "description": "This is my project description.",
    "author": "bitExpert AG",
    "licence": "Proprietary",
    "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
    },
    "repository": {
        "type": "git",
        "url": "https://gitrepo.loc/myproject.git"
    },
    "devDependencies": {
    }
}

Task Runner

Grunt dependencies

{
    "devDependencies": {
        "grunt": "^0.4.0",
        "grunt-cli": "^0.1.0",
        "grunt-contrib-watch": "^0.6.0",
        "grunt-newer": "^0.7.0",
        "grunt-notify": "^0.3.0",
        "load-grunt-tasks": "^0.6.0"
    }
}

Basic Gruntfile.js

module.exports = function (grunt) {
    var jsFiles,
        scssFiles;

    jsFiles = {
        src: [ 'srcweb/js/src/**/*.js' ]
    };

    scssFiles = {
        src: [ 'srcweb/scss/**/*.scss' ]
    };

    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),
    });

    // Load grunt tasks automatically
    require('load-grunt-tasks')(grunt);
};

Linting your JS code



jshint configuration

Adding jshint to the project

{
    "devDependencies": {
        "grunt-contrib-jshint": "^0.11.0",
        "bitexpert-cs-jshint": "^0.1.0",
        "jshint-jenkins-checkstyle-reporter": "^0.1.0"
    }
}

.jshintrc configuration

{
    "extends": "node_modules/bitexpert-cs-jshint/config/generic.json",
    "globals": {
        "window": true,
        "define": true,
        "require": true
    }
}

Extending Gruntfile.js

grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),

    jshint: {
        cli: {
            files: jsFiles
        },
        ci: {
            files: jsFiles,
            options: {
                reporter: require('jshint-jenkins-checkstyle-reporter'),
                reporterOutput: 'build/logs/checkstyle-jshint.xml'
            }
        }
    }
});

Code style linter


jscs configuration

Adding jscs to the project

{
    "devDependencies": {
        "grunt-jscs": "^1.5.0",
        "bitexpert-cs-jscs": "^0.1.0"
    }
}

Extending Gruntfile.js

grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),

    jscs: {
	cli: {
	    files: jsFiles,
	    options: {
		    config: 'node_modules/bitexpert-cs-jscs/config/config.json'
	    }
	},
	ci: {
	    files: jsFiles,
	    options: {
		        reporter: 'checkstyle',
		        reporterOutput: 'build/logs/checkstyle-jscs.xml',
		        config: '<%= jscs.cli.options.config %>'
	    }
	}
    }
});

Karma Test Runner

Adding Karma to the project

{
    "devDependencies": {
        "grunt-karma": "^0.9.0",
        "karma": "^0.12.24",
        "karma-fixture": "^0.2.1-1",
        "karma-html2js-preprocessor": "^0.1.0",
        "karma-junit-reporter": "^0.2.2",
        "karma-chrome-launcher": "^0.1.7",
        "karma-phantomjs-launcher": "^0.1.4",
        "karma-firefox-launcher": "^0.1.4",
        "karma-webdriver-launcher": "^1.0.1"
    }
}

Mocha - JS test framework

Adding Mocha to the project

{
    "devDependencies": {
        "karma-mocha": "^0.1.9"
}

Chai - Assertion library

Sinon.JS - Spies, Stubs...

Adding Chai to the project

{
    "devDependencies": {
        "karma-sinon-chai": "^0.2.0"
}

Code coverage with Istanbul

Adding Istanbul to the project

{
    "devDependencies": {
        "karma-coverage": "^0.2.6"
    }
}

Extending Gruntfile.js

grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),

    karma: {
	options: {
	    files: [
		        'src/**/*.js',
		        'tests/test-main.js',
		        'tests/fixture/**/*.html',
		        'tests/spec/**/*.js'
	    ],
	    basePath: 'srcweb/js',
	    frameworks: [
		        'mocha',
		        'sinon-chai',
		        'fixture'
	    ],
	    colors: true
	}
    }
});

Configure fixtures

// tests/test-main.js
// configure fixture for easier use
if (fixture && Fixture) {
    fixture = new Fixture('tests/fixture');
}

Karma CLI configuration

grunt.initConfig({
    karma: {
	// options as defined before...
	cli: {
	    logLevel: 'DEBUG',
	    reporters: [
		        'progress'
	    ],
	    browsers: [
		        'PhantomJS'
	    ],
	    singleRun: true,
	    preprocessors: {
		        'tests/fixture/**/*.html': [
		            'html2js'
		        ]
	    }
	}
    }
});

Karma CI configuration

grunt.initConfig({
    karma: {
	// options as defined before...
        ci: {
            autoWatch: false,
            logLevel: 'DEBUG',
            reporters: [ 'progress', 'coverage' ],
            preprocessors: {
                'tests/fixtures/**/*.html': [ 'html2js' ],
                'src/**/*.js': [ 'coverage' ]
            },
            browsers: [ 'PhantomJS' ],
            singleRun: true,
            coverageReporter: {
                type: 'html', dir: '../../build/coverage/js/unit'
            },
            junitReporter: {
                outputFile:'build/logs/junit-js.xml', suite:'myproject'
            }
        }
    }
});

Let`s write a test!

Code to test

(function () {
    /**
     * @namespace MyCode
     * @global
     */
    MyCode = {
        /**
         * Concats a and b.
         *
         * @function concatFn
         * @memberof MyCode
         * @static
         * @param a {String}
         * @param b {String}
         * @returns {String}
         */
        concatFn: function (a, b) {
            return [ a, b ].join(' ');
        }
    };
}());

The unit test

describe("MyCode", function () {
    it("is defined", function () {
     	expect(MyCode).to.be.ok;
    });

    it("concatenates strings", function () {
	var a = 'this is',
	    b = 'awesome',
	    expectation = a + ' ' + b;
	
	expect(MyCode.concatFn(a, b)).to.be.equal(expectation);
    });
});

Running Karma

$ grunt karma:cli
Running "karma:cli" (karma) task
INFO [karma]: Karma v0.12.35 server started at http://localhost:9876/
INFO [launcher]: Starting browser PhantomJS
INFO [PhantomJS 1.9.8 (Linux 0.0.0)]: Connected on socket xFNl5oPPo70YtgkfldU with id 8889029
PhantomJS 1.9.8 (Linux 0.0.0): Executed 2 of 2 SUCCESS (0.038 secs / 0 secs)

Done, without errors.

Running Karma continously

grunt.initConfig({
    watch: {
	js: {
	    files: jsFiles.src,
	    tasks: [
		        'karma:cli'
	    ]
	}
    },
});
$ grunt watch

Istanbul Coverage Report

Selenium



Extending Gruntfile.js

grunt.initConfig({
    karma: {
	// options as defined before...
        selenium: {
            autoWatch: false,
            logLevel: 'DEBUG',
            reporters: [ 'progress', 'coverage' ],
            preprocessors: {
                'tests/fixtures/**/*.html': [ 'html2js' ],
                'src/**/*.js': [ 'coverage' ]
            },
            browsers: [ 'PhantomJS' ],
            singleRun: true,
            coverageReporter: {
                type: 'html', dir: '../../build/coverage/js/unit'
            },
            junitReporter: {
                outputFile:'build/logs/junit-js.xml', suite:'myproject'
            },
	    hostname: getLocalIPAddress(),

Extending Gruntfile.js

            customLaunchers: {
                'Chrome41OnWin8': {
                    base: 'WebDriver',
                    config: {
                        hostname: 'selenium-hub.loc', port: 4444
                    },
                    platform: 'Win8',
                    browserName: 'chrome',
                    version: '41'
                }
            }
        }
    }
});

Extending Gruntfile.js

function getLocalIPAddress () {
    var os = require('os'),
	ifaces = os.networkInterfaces(),
	device;

    for (var dev in ifaces) {
	if (dev.substring(0, 3) !== 'eth') { continue; }

	device = ifaces[dev];
	for (var index in device) {
	    var details;
	    if (!device.hasOwnProperty(index)) { continue; }

	    details = device[index];
	    if (details.family == 'IPv4') { return details.address; }
	}
    }
    return '127.0.0.1';
};

scss-lint

scss-lint configuration

Extending Gruntfile.js

grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),

    scsslint: {
	allFiles: scssFiles,
	options: {
	    config: 'node_modules/bitexpert-cs-scsslint/config/config.yml',
	    colorizeOutput: true,
	    reporterOutput: 'build/logs/junit-scsslint.xml'
	}
    }
});

Gruntfile.js Aliases

grunt.registerTask('default', ['jscs:cli']);
grunt.registerTask('sniff', ['scsslint', 'jscs:ci', 'jshint:ci']);
grunt.registerTask('lint', ['jshint:cli']);
grunt.registerTask('test', ['karma:cli']);
grunt.registerTask('ci:build', ['compass', 'jscs:ci', 'jshint:ci', 
'karma:ci']);





Jenkins Job Config

Push config files to node

Installing dependencies

Running Grunt

Process the build results

Email notifications

Email notifications

Running Grunt (Int. Build)

In the future (maybe)...








Thank you!







Do not forget to rate the talk:
https://joind.in/14252