gulpfile.jsをテストする

**「gulpでテストする」ではありません！**
**「gulpfile.jsのタスクがちゃんと動くかテストを書く」というニッチな話です！**

プロジェクトのスターターキットや[Yeoman](http://yeoman.io/)ジェネレータを作った際、環境によってビルドに不具合や差分が出るのは嫌だなと思い、gulpの実行をテストフレームワークを使って検証してみました。

**サンプルコード**
https://github.com/htanjo/gulpfile-test

**実際のプロジェクト**
https://github.com/rakuten-frontend/rff-gulp

## テスト対象のgulpfile.js

**gulpfile.js**

```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を動かします。

```js
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](https://github.com/yeoman/yeoman-assert)が便利です。

```js
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](https://mochajs.org/)、[Istanbul](https://github.com/gotwarlost/istanbul)を使っていますが、好きなものでOKです。

```json
{
  "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](https://github.com/jhnns/rewire)を使いました。

```js
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で取ってくるのでここには不要です。

```json
{
  "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](https://travis-ci.org/)や[Coveralls](https://coveralls.io/)と連携させれば、Node.jsのバージョンごとにテストを行ったり、カバレッジ情報を継続的に管理できるようになります。

**[デモプロジェクト](https://github.com/rakuten-frontend/rff-gulp)のステータス**
[![Build Status](https://travis-ci.org/htanjo/gulpfile-test.svg?branch=master)](https://travis-ci.org/htanjo/gulpfile-test) [![Coverage Status](https://coveralls.io/repos/github/htanjo/gulpfile-test/badge.svg?branch=master)](https://coveralls.io/github/htanjo/gulpfile-test?branch=master)

**package.json**

```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**

```yaml
language: node_js
sudo: false
node_js:
  - "5"
  - "4"
  - "0.12"
after_script:
  - npm run coveralls
```