LoginSignup
6
7

More than 5 years have passed since last update.

gulpfile.jsをテストする

Last updated at Posted at 2016-03-04

「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
実行スクリプト。MochaIstanbulを使っていますが、好きなもので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 CICoverallsと連携させれば、Node.jsのバージョンごとにテストを行ったり、カバレッジ情報を継続的に管理できるようになります。

デモプロジェクトのステータス
Build Status Coverage Status

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
6
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
7