Vueify 単一ファイルコンポーネントを単体テストする

この記事は、[Vue.js Advent Calender 2015](http://qiita.com/advent-calendar/2015/vue) 7日目の記事です。

Vueify で書かれた Vue.js のコンポーネントを単体テストしましょう。

本記事では

* [Vueify](https://github.com/vuejs/vueify) でコンポーネントを書き
* [Babel](https://babeljs.io) でトランスパイルし
* [Browserify](http://browserify.org/) で１ファイルに結合して運用しているコンポーネントを
* [Jasmine](http://jasmine.github.io) で単体テストを書き
* [Karma](http://karma-runner.github.io/0.13/index.html) で動かす

までを解説していきます。
前半の Vueify + Babel + Browserify までは、
[はじめてのVue.js - 単一ファイルコンポーネントを作れる環境構築編 (npm + gulp + browserify + babel + vueify)](http://qiita.com/ayasuda/items/92221229e8f8c65c6f7c)に準じた環境を前提としますのでご了承ください。
また、一部で [はじめてのVue.js - タスクリストの実装から学ぶコンポーネントベースアプリ](http://qiita.com/ayasuda/items/95a3298870a06ade8f2a) で作成したコードをベースにテストを作成します。

## 環境構築

まず、Karma を導入しましょう。Karma は様々なブラウザで様々なテストスイートを走らせ、レポートを作成する「テストランナー」に当たるツールです。インストールは下記コマンドから行います。

> $ npm install --save-dev karma

さて、 Karma はインストールしただけでは意味があまりありません。あくまでもテストランナーであるので、実際にテストを記述する「テストフレームワーク」とテスト実行環境用のブラウザプラグインをそれぞれインストールする必要があるためです。

今回はテストフレームワークとして Jasmine を使い、ブラウザには Google Chrome を使うこととします。そこで、 karma-jasmine と karma-chrome-lancher もインストールします。インストールは次のコマンドで行います。

> $ npm install karma-jasmine karma-chrome-launcher --save-dev

最後に Karma の設定フィアルを用意します。 Karma には設定ファイルを生成するウィザードが付属しているので、ウィザード経由で初期設定するのが良いでしょう。
次のコマンドでウィザードを起動してください。

> $ ./node_modules/karma/bin/karma init

ウィザードの中で質問される項目について超訳と記入例を付属しておきます。

```bash
Which testing framework do you want to use ?
Press tab to list possible options. Enter to move to the next question.
> jasmine # 使用するテストフレームワーク。ここでは jasmine

Do you want to use Require.js ?
This will add Require.js plugin.
Press tab to list possible options. Enter to move to the next question.
> no # Require.js を使用するか？ Browserify 使ってるので no

Do you want to capture any browsers automatically ?
Press tab to list possible options. Enter empty string to move to the next question.
> Chrome # テストで使用するブラウザ。PhantomJS とか便利だけど、今回は Chrome
> # 使用するブラウザの選択終わったら空行で Enter 入力して選択を終わらせる

What is the location of your source and test files ?
You can use glob patterns, eg. "js/*.js" or "test/**/*Spec.js".
Enter empty string to move to the next question.
> spec/**/*_spec.js # テストファイルの場所。好みでいいけど、著者はこうしてる。
> # 選択が終わったら空行で Enter 入力して選択を終わらせる。

Should any of the files included by the previous patterns be excluded ?
You can use glob patterns, eg. "**/*.swp".
Enter empty string to move to the next question.
> # 無視するファイルを選ぶ。入力が終わったら空行で Enter を入力して選択を終わらせる。

Do you want Karma to watch all the files and run the tests on change ?
Press tab to list possible options.
> no # Karma に watch させたいか？ gulp でやるなら no で大丈夫のはず


Config file generated at "/Users/ayasuda/src/hello_vuejs/karma.conf.js".
# ↑
# ここに設定ファイルができますよ！
```

設定ファイルが生成されたら、早速テストを書いてみましょう。次のような、絶対にパスするテストを *spec/my_first_spec.js* に書いてみてください。

```spec/my_first_spec.js
describe('my first spec', function () {                                                                                                                                              
  it ('is ok', function() {
    expect(true).toBe(true);
  })
})
```

テストが書けたらここまでの動きを試してみましょう。ブラウザが立ち上がってテストが実行されるはずです！テストの実行は次のコマンドで行います。

> $ ./node_modules/karma/bin/karma start karma.conf.js --single-run

## (Appendix) gulp と npm に包む

毎度、上のコマンドを打つのはかなり手間です。また、ビルドシステムに包むことで、テスト前後に様々な処理をかませられるようにしておいた方が後々便利でしょう。

いつも通り karma の実行を gulp タスクに包み、かつ `npm run test` で実行できるようにしておきましょう。

まずは、 gulp の karma プラグインをインストールします。

> $ npm install --save-dev gulp-karma

プラグインがインストールできたら、gulp に test タスクを追加しましょう。

```gulpfile.js
var karma = require('karma').Server

gulp.task('test', function() {
  runner = new karma({
    configFile: __dirname + '/karma.conf.js',
    singleRun: true
  })
  runner.start()
})
```

gulp のタスクを追加したら、 npm でさらにラップします。

```package.json
{
  "scripts": {
    "build": "gulp build",
    "test":  "gulp test"
  },
}
```

ここまでで `npm run test` と打つことで karma のテストが動くようにできました。
早速試してみてください。

## Babel と Browserify をテスト内でも適用する

単一ファイルコンポーネントを ES6 で書くために Babel を使っている場合や、ひとつのファイルにまとめるために Browserify を使用している場合、このままでは karma でテストが書けません。

Browserify は単一の、たとえば *bundle.js* とかにファイルをまとめてくれますが、単体テスト前に動くわけではないからです。

まずは **動かないことを試してみましょう** 次のファイルを用意してください。
[はじめてのVue.js - タスクリストの実装から学ぶコンポーネントベースアプリ](http://qiita.com/ayasuda/items/95a3298870a06ade8f2a) で作成した `Task` クラスをただ初期化するだけのコードですが、 *model/task.js* は ES6 で書かれています。ファイルが書けたら `npm run test` してみましょう。

```spec/task_spec.js
describe('Task', function() {
  var Task = require('../model/task').default
  it('is ok', function() {
    var task = new Task({title: "test"})
    expect(task.title).toBe("test")
  })
})
```

・・・もちろん動かなかったはずです。これを動くようにするために、 karma に browserify プラグインをインストールします。インストールは下記コマンドから行います。

> $ npm install --save-dev karma-browserify

プラグインのインストールができたら karma の設定ファイルにプラグインを使用するための各種設定値を記していきます。

```karma.conf.js
module.exports = function(config) {
  config.set({
    frameworks: ['browserify', 'jasmine'], // browserify プラグインを追加する

    preprocessors: {
      // **_spec.js を読み込む際に browserify してからファイルを読みこむ
      'test/**/*_spec.js': ['browserify'], 
    },
 
    // browserify の設定
    // ファイル中の require を解決するのがメインの仕事
    browserify: {
      debug: true,
      // require の解決だけじゃなくて、 vueify して babelify もする
      transform: ['vueify', ['babelify', {"presets": "es2015"}] ]
    },
  })
}
```

設定を書いたら、再度 `npm run test` してみてください。今度はうまくテストが動いたはずです！！

> **注意**
> package.json に browserify の設定を書いている場合、
> karma.conf.js に browserify の設定を書いてはいけません！
> ２重に読み込まれ、結果として vueify した結果に vuiefy が適用されることで
> **require した時に空オブジェクト** が返ることになります！
> *(ここのハマって４時間かかった)*

## Vueify で書かれた単一ファイルコンポーネントをテストする

ここまでできればあとは簡単です。[公式ガイドに書かれているように](http://jp.vuejs.org/guide/application.html#単体テスト) 例えば下記のような組み合わせのテストを書くことができます！

```js:spec/components/task.spec
describe('my-task', function () {
  // ソースモジュールを require
  var myTask = require('../../components/task.vue')
  it('created hook が定義されている', function () {
    expect(typeof myTask.created).toBe('function')
  })
})
```

```html:components/task.vue
<style>
  .my-task {
    color: #f0c;
  }
</style>

<template>
<div class="my-task">
  <label>タイトル</label>
  <input type="text"
  v-model="task.title"
  placeholder="タイトルを入力してください"
  v-on:keyup.enter="saveTask"
  v-on:blur="saveTask"
  >
  <span v-on:click="removeTask">削除</span>
</div>
</template>

<script>
var Task = require('../model/task').default

export default {
  props: {
    task: { type: Object, default: () => { return new Task } }
  },
  methods: {
    saveTask: function() {
      this.$dispatch('task-changed')
    },
    removeTask: function() {
      this.$dispatch('task-removed', this.task)
    },
  },
  created: function() {
    console.log("created")
  }
}
</script>
```

##