QUnit, Karma, JSHint, JSDoc3 を組み合わせて Jenkins 上で CI を行う
- HTML5 APIs
- UI ライブラリ
- テスト支援
- ドキュメント
- コード品質ツール
- バリデータ
- キー入力支援
- テストダブル
- ハイブリッドアプリケーション
- 継続的インテグレーション
QUnit, Karma, JSHint, JSDoc3 を組み合わせて Jenkins 上で CI を行う
- 調査時期
- 2015 年 6 月
概要
以下の3つを Jenkins を利用して継続的に実行する方法を紹介します。
前提条件
- プロジェクトがあるマシンと Jenkins があるマシンに Node.js がインストール済みであること
プロジェクト側に必要な設定
プロジェクト側で必要となる設定を紹介します。
各種ツールを利用するための package.json を用意しツールをインストールする
以下のような package.json をプロジェクトのルートディレクトリに用意します。
"private": true
}
プロジェクトのルートディレクトリ上で必要なモジュールを以下のコマンドでインストールしていきます。
※インストールしたモジュールはルートディレクトリの node_modules 以下に入り、package.json も更新されます
> npm install --save-dev {モジュール名}
必要なモジュール一覧
- del
- gulp
- gulp-jshint
- gulp-jshint-xml-file-reporter
- gulp-load-plugins
- gulp-util
- jsdoc
- jshint-stylish
- karma
- karma-chrome-launcher
- karma-coverage
- karma-ie-launcher
- karma-junit-reporter
- karma-qunit
- lodash
- mkdirp
- qunitjs
JSHint の設定ファイルを用意する
JSLint/JSHint の解説 や JSHint 公式のドキュメント を参考に JSHint の設定記述した .jshintrc ファイルをプロジェクトのルートディレクトリに作成します。
設定のサンプル
"bitwise": true,
"camelcase": true,
"curly": true,
"eqeqeq": true,
"es3": false,
"forin": true,
"freeze": true,
"immed": true,
"latedef": true,
"newcap": true,
"noarg": true,
"noempty": true,
"nonbsp": true,
"nonew": true,
"plusplus": false,
"quotmark": "single",
"undef": true,
"unused": true,
"strict": true,
"maxparams": 6,
"maxdepth": 2,
"maxstatements": 1000,
"maxcomplexity": 15,
"maxlen": 10000,
"eqnull": true
}
JSDoc の設定ファイルを用意する
以下のようにJSDoc の設定を記述した jsdoc-conf.json ファイルをプロジェクトのルートディレクトリに作成します。
"opts": {
"encoding": "utf8",
"recurse": false,
"private": false,
"lenient": true
}
}
ビルド用の定数を定義したファイルを用意する
以下の定数を定義する build-properties.js ファイルをプロジェクトのルートディレクトリに作成します。
※ gulp 設定ファイルと karma 設定ファイルで参照する定数をまとめて記述するためです
- BUILD_DIR: ビルドディレクトリのパス
- SRC_PATTERN: ソースファイルが置いてあるパスのパターン
- JSHINT_TARGET_FILES: JSHint のターゲットとなるファイルパターンのリスト
- JSHINT_REPORT_PATH: JSHINT_REPORT_DIR: JSHint のレポートを出力するディレクトリ
- JSDOC_TARGET_FILES: JSHint のレポートを出力するパス
- JSDOC_OUTPUT_DIR: JSDoc のターゲットとなるファイルパターンのリスト
- JSDoc を出力するディレクトリのパス
- KARMA_TARGET_FILES: Karma が読み込むファイルパターンのリスト
- TEST_REPORT_PATH: テストの結果を出力するディレクトリのパス
- COVERAGE_REPORT_DIR: カバレッジのレポートを出力するディレクトリのパス
KARMA_TARGET_FILES に関する注意点
- リストの順に読み込むので依存関係に従った順で記述する
- CSS なども必要であれば追加しておくと読み込まれる
- debug ボタン押下時に QUnit の UI を出すために以下の JavaScript ファイルを作成し、QUnit とテストコードの間に読み込ませる
/* jshint browser: true */
'use strict';
// qunit 要素を追加
var qunitNode = document.createElement('div');
qunitNode.setAttribute('id', 'qunit');
document.body.appendChild(qunitNode);
// fixture 要素を追加
var fixtureNode = document.createElement('div');
fixtureNode.setAttribute('id', 'qunit-fixture');
document.body.appendChild(fixtureNode);
})();
Karma の設定ファイルを用意する
以下のような Karma 設定を記述した karma.conf.js ファイルをプロジェクトのルートディレクトリに作成します。
'use strict';
var prop = require('./build-properties');
module.exports = function(config) {
var options = {
frameworks: ['qunit'],
files: prop.KARMA_TARGET_FILES,
excludes: [],
reporters: ['dots', 'junit'],
port: 9876,
colors: true,
logLevel: config.LOG_DEBUG,
client: {
captureConsole: false
},
autoWatch: true,
browsers: ['Chrome', 'IE'],
singleRun: false,
junitReporter: {
outputFile: prop.TEST_REPORT_PATH,
suite: ''
}
};
config.set(options);
};
また、カバレッジ測定用の設定を記述した karma-coverage.conf.js ファイルをプロジェクトのルートディレクトリに作成します。
'use strict';
var karmaConf = require('./karma.conf');
var prop = require('./build-properties');
function get(obj, property, defaultValue) {
if (obj[property] == null) {
obj[property] = defaultValue;
}
return obj[property];
}
function makeSubdir(path) {
return function(browser) {
return browser + '/' + path;
};
}
module.exports = function(config) {
karmaConf(config);
var preprocessors = get(config, 'preprocessors', {});
var preprocessorTarget = get(preprocessors, prop.SRC_PATTERN, []);
preprocessorTarget.push('coverage');
var reporters = get(config, 'reporters', []);
reporters.push('coverage');
config.coverageReporter = {
dir: prop.COVERAGE_REPORT_DIR,
reporters: [
{ type: 'html', subdir: makeSubdir('report') },
{ type: 'cobertura', subdir: makeSubdir('xml') },
{ type: 'text-summary' }
]
};
config.singleRun = true;
};
ビルドを実行する Gulpfile.js を用意する
以下のような Gulp 設定を記述した gulpfile.js ファイルをプロジェクトのルートディレクトリに作成します。
'use strict';
// ---- 依存モジュールのロード ---- //
// -- Node.js 標準モジュール -- //
var os = require('os');
var spawn = require('child_process').spawn;
var path = require('path');
// -- Gulp 関連モジュール -- //
var gulp = require('gulp');
var plugins = require('gulp-load-plugins')();
var log = plugins.util.log;
var colors = plugins.util.colors;
// -- ディレクトリ操作モジュール -- //
var del = require('del');
var mkdirp = require('mkdirp');
// -- Karma モジュール -- //
var karma = require('karma');
// ---- ビルド用定数定義 ---- //
var prop = require('./build-properties');
// ---- 実行環境の判定 ---- //
var isWindows = !!os.type().match(/^Windows/);
// ---- NPM コマンド実行関数定義 ---- //
function npmCommand(command, args, cb) {
var actualCommand = command;
var actualArgs = args;
if (isWindows) {
actualCommand = 'cmd.exe';
actualArgs = ['/c', '.\\node_modules\\.bin\\' + command].concat(args);
}
log(colors.blue('Run: ') + command + ' ' + args.join(' '));
var childProcess = spawn(actualCommand, actualArgs, {
stdio: ['ignore', 1, 2]
});
childProcess.on('error', function(e) {
log(colors.red('Fail NPM Command: ' + command));
log(e);
throw e;
});
childProcess.on('exit', function(exitCode) {
if (exitCode === 0) {
cb();
return;
}
log(colors.red('Fail NPM Command: ' + command));
log('exit code: ' + exitCode);
throw new Error('exit code: ' + exitCode);
});
}
// ---- Karma 実行関数 ---- //
function startKarma(configFile, cb) {
karma.server.start({
configFile: path.resolve(configFile)
}, function(exitCode) {
log(colors.green('Karma Exit: ' + exitCode));
cb();
});
}
// ---- タスクの定義 ---- //
gulp.task('clean', function(cb) {
del(prop.BUILD_DIR, cb);
});
gulp.task('jshint', function() {
mkdirp.sync(prop.JSHINT_REPORT_DIR);
return gulp.src(prop.JSHINT_TARGET_FILES)
.pipe(plugins.jshint('.jshintrc'))
.pipe(plugins.jshint.reporter('jshint-stylish'))
.pipe(plugins.jshint.reporter(plugins.jshintXmlFileReporter))
.on('end', plugins.jshintXmlFileReporter.writeFile({
format: 'checkstyle',
filePath: prop.JSHINT_REPORT_PATH,
alwaysReport: true
}));
});
gulp.task('jsdoc', function() {
mkdirp.sync(prop.JSDOC_OUTPUT_DIR);
return gulp.src(prop.JSDOC_TARGET_FILES)
.pipe(plugins.util.buffer(function(err, files) {
var args = ['-c', 'jsdoc-conf.json', '-d', prop.JSDOC_OUTPUT_DIR];
var paths = files.map(function(file) {
return file.path;
});
npmCommand('jsdoc', args.concat(paths), function() {
log(colors.green('SUCCESS: jsdoc'));
});
}));
});
gulp.task('karma', function(cb) {
startKarma('karma.conf.js', cb);
});
gulp.task('coverage', function(cb) {
startKarma('karma-coverage.conf.js', cb);
});
gulp.task('default', ['jshint', 'jsdoc', 'coverage']);
Gulp の実行方法
プロジェクトのルートディレクトリで以下のようなコマンドを実行することで Gulp タスクを実行できます。
> .\node_modules\.bin\gulp {タスク名}
これまでの設定に従っていれば以下のタスクを実行することができます。
- clean: ビルドディレクトリを削除します
- jshint: JSHint を実行し結果をコンソールと checkstyle 形式の XML で出力します
- jsdoc: JSDoc を実行し API ドキュメントを出力します
- coverage: Karma を一回実行しテスト結果とカバレッジのレポートを出力します
- karma: Karma を立ち上げ、ファイルが更新されるたびにテストを走らせます
- default(引数なしでも可): jshint, jsdoc, coverage を実行します
Jenkins に必要な設定
Jenkins 上では以下のコマンドを実行するジョブを作成します。
- npm prune を実行する
- npm install を実行する
- Gulp の clean タスクを実行する
- Gulp の default タスクを実行する
ジョブを実行後、JSHint の結果は checkstyle 形式の XML で、テストの結果は JUnit 形式の XML で、カバレッジの結果は cobertura 形式の XML で読み込むことができます。
Karma を Selenium Grid のブラウザからテストしたい場合に必要な設定
Karma を開発者のPC上にあるブラウザではなく Selenium Grid 上のブラウザから実行する場合に必要な設定です。
※ Selenium Grid に関しての説明は こちら を参照してください
以下のモジュールを追加でインストールします。
karma.conf.js の設定に以下のように customLaunchers を追加します。
'selenium-grid-IE': {
base: 'SeleniumWebdriver',
browserName: 'internet explorer',
getDriver: function() {
var builder = new require('selenium-webdriver').Builder();
return builder
.forBrowser('internet explorer')
.usingServer('http://localhost:4444/wd/hub') // Selenium Grid の Hub の URL を指定します
.build();
}
}
},
browsers: ['selenium-grid-IE'], // browsers: ['Chrome', 'IE'] から customLaunchers で追加したものに差し替える
上記設定後に Selenium Grid の Hub と Node を立ち上げて karma を実行すると Selenium Grid 上のブラウザでテストが実行されます。