「gulpでテストする」ではありません!
「gulpfile.jsのタスクがちゃんと動くかテストを書く」というニッチな話です!
プロジェクトのスターターキットやYeomanジェネレータを作った際、環境によってビルドに不具合や差分が出るのは嫌だなと思い、gulpの実行をテストフレームワークを使って検証してみました。
サンプルコード
https://github.com/htanjo/gulpfile-test
実際のプロジェクト
https://github.com/rakuten-frontend/rff-gulp
テスト対象のgulpfile.js
gulpfile.js
var gulp = require('gulp');
gulp.task('copy', function () {
return gulp.src('app/index.html')
.pipe(gulp.dest('dist'));
});
この記事では、gulp copy
でHTMLファイルをコピーするだけの、簡単なgulpfileを対象とします。
テストは、ファイルが指定先に存在するか、コンテンツが特定の文字列を含んでいるかの2点をチェックすることにします。
gulpタスクの実行方法
child_process
を使ってgulpコマンドを実行するとカバレッジを取るのが難しくなるため、以下のようにプログラム的にgulpを動かします。
var gulp = require('gulp');
// gulpfile.jsをそのままrequire()する
require('./gulpfile');
// タスク一覧の取得
console.log(gulp.tasks);
// タスクの実行
gulp.start('copy');
// タスクの実行 + コールバック
var runSequence = require('run-sequence');
runSequence('copy', function (err) {
console.log('Done!');
});
ただし、gulpfile.jsの位置が、テスト実行時のprocess.cwd()
と一致していない場合は、上記のコードでは正しくタスクを実行できません。(後述)
ケース1 : gulpfileが同階層にある場合
こういう構成です。上記のコードがそのまま使えます。
project/
├── package.json
├── test
│ └── gulp.js # テストコード
├── gulpfile.js # テスト対象
└── app/
└── index.html # copyタスクの対象("gulp"という文字列を含んだHTML)
test/gulp.js
テストコード。ファイルのアサートにはyeoman-assertが便利です。
var gulp = require('gulp');
var runSequence = require('run-sequence');
var assert = require('yeoman-assert');
// gulpfile.jsの読み込み
require('../gulpfile');
describe('gulp', function () {
describe('task "copy"', function () {
// copyタスクを実行する
before(function (done) {
runSequence('copy', function (err) {
if (err) throw err;
done();
});
});
// "dist/index.html"が出力されているか
it('outputs file into "dist" directory', function () {
assert.file('dist/index.html');
});
// "dist/index.html"が"gulp"という文字列を含んでいるか
it('outputs file with expected content', function () {
assert.fileContent('dist/index.html', /gulp/);
});
});
});
package.json
実行スクリプト。Mocha、Istanbulを使っていますが、好きなものでOKです。
{
"scripts": {
"test": "istanbul cover _mocha"
},
"devDependencies": {
"gulp": "^3.9.1",
"istanbul": "^0.4.2",
"mocha": "^2.4.5",
"run-sequence": "^1.1.5",
"yeoman-assert": "^2.1.1"
}
}
実行結果
$ npm test
gulp
task "copy"
✓ outputs file into "dist" directory
✓ outputs file with expected content
2 passing (31ms)
=============================== Coverage summary ===============================
Statements : 100% ( 3/3 )
Branches : 100% ( 0/0 )
Functions : 100% ( 1/1 )
Lines : 100% ( 3/3 )
================================================================================
ケース2 : 別ディレクトリのgulpfileをテストする
このような構成とします。プロジェクトが入れ子になっていて、package.jsonが2つあります。
project/
├── package.json # テストスイート用のpackage.json
├── test
│ └── subproject.js # テストコード
└── subproject/
├── package.json # gulpタスク用のpackage.json
├── gulpfile.js # テスト対象
└── app/
└── index.html # copyタスクの対象("subproject"という文字列を含んだHTML)
test/subproject.js
さきほどの例と違うのは、以下の点です。
- "subproject"内で使われるgulpインスタンスを取得する必要がある
-
process.cwd()
を"subproject"に移さないと、タスクによっては正しく動かない
テスト対象のコード(gulpfile.js)内にあるモジュールにアクセスするため、rewireを使いました。
var rewire = require('rewire');
// "rewire"でgulpfile.jsをロード
var gulpfile = rewire('../subproject/gulpfile');
// gulpfile.js内のgulpインスタンスを取得
var gulp = gulpfile.__get__('gulp');
// .use()を使い、取得したgulpインスタンスを操作対象にする
var runSequence = require('run-sequence').use(gulp);
var assert = require('yeoman-assert');
var path = require('path');
var originalCwd = process.cwd();
describe('gulp in subproject', function () {
// テスト前に、process.cwd()を"subproject"に移す
before(function () {
process.chdir(path.join(__dirname, '../subproject'));
});
// テスト後に、process.cwd()を元に戻す
after(function () {
process.chdir(originalCwd);
});
describe('task "copy"', function () {
// copyタスクを実行する
before(function (done) {
runSequence('copy', function (err) {
if (err) throw err;
done();
});
});
// "dist/index.html"が出力されているか
it('outputs file into "dist" directory', function () {
assert.file('dist/index.html');
});
// "dist/index.html"が"subproject"という文字列を含んでいるか
it('outputs file with expected content', function () {
assert.fileContent('dist/index.html', /subproject/);
});
});
});
package.json
実行スクリプト。こちらはさっきとほとんど一緒です。gulpはrewireで取ってくるのでここには不要です。
{
"scripts": {
"test": "istanbul cover _mocha"
},
"devDependencies": {
"istanbul": "^0.4.2",
"mocha": "^2.4.5",
"rewire": "^2.5.1",
"run-sequence": "^1.1.5",
"yeoman-assert": "^2.1.1"
}
}
実行結果
gulp, gulpfileのロードが特殊でしたが、カバレッジまでちゃんと取れました。
$ npm test
gulp in subproject
task "copy"
✓ outputs file into "dist" directory
✓ outputs file with expected content
2 passing (32ms)
=============================== Coverage summary ===============================
Statements : 100% ( 3/3 )
Branches : 100% ( 0/0 )
Functions : 100% ( 1/1 )
Lines : 100% ( 3/3 )
================================================================================
CI連携
Travis CIやCoverallsと連携させれば、Node.jsのバージョンごとにテストを行ったり、カバレッジ情報を継続的に管理できるようになります。
デモプロジェクトのステータス
package.json
{
"scripts": {
"postinstall": "npm install --prefix subproject",
"test": "istanbul cover _mocha",
"coveralls": "cat coverage/lcov.info | coveralls"
},
"devDependencies": {
"coveralls": "^2.11.8",
"gulp": "^3.9.1",
"istanbul": "^0.4.2",
"mocha": "^2.4.5",
"rewire": "^2.5.1",
"run-sequence": "^1.1.5",
"yeoman-assert": "^2.1.1"
}
}
.travis.yml
language: node_js
sudo: false
node_js:
- "5"
- "4"
- "0.12"
after_script:
- npm run coveralls