Jasmine による Backbone.js の単体テストことはじめ

# <i class="fa fa-fw fa-flag"></i> 目的

Backbone.js の単体テストを行うための環境を用意する。

# <i class="fa fa-fw fa-smile-o"></i> 動機

僕は普段 Rails をよく使うプログラマーなのですが、今まで JavaScript の単体テストも書かなきゃと思いつつ、ずっとスルーし続けてきました。しかし、最近はフロントエンドに Backbone.js や Marionette.js を使い、複雑な処理を書くことも増えてきたので、ついに重い腰を上げて JavaScript の単体テストを書くことにしました。

また Babel の導入により機能のモジュール分割が容易になったことも、単体テストを書き始めようと思い立った要因のひとつでもあります。

# <i class="fa fa-fw fa-cubes"></i> 主なツール

| ツール<br>バージョン | 説明 |
|:--|:----|
| [Jasmine](https://github.com/jasmine/jasmine)<br>2.4.1| テストフレームワーク<br><br>最近は [Mocha](https://github.com/mochajs/mocha) をよく聞きますが、Google トレンドで調べたところ Jasmine の方が人気がありそうだったので、今回はこちらを採用しました。BDD (ビヘイビア駆動開発) 形式で、RSpec 使いには文法が非常に馴染みやすいと思います。 |
| [jasmine-jquery](https://github.com/velesin/jasmine-jquery)<br>2.1.1 | Jasmine の拡張ツール | jQuery 向けのカスタムマッチャーを提供します。 |
| [Karma](https://github.com/karma-runner/karma)<br>0.13.15 | テストランナー<br><br>任意のブラウザーで JavaScript を動かすことができます。<br>Backbone.View の単体テストを書く際に DOM を絡めたテストを書く必要があるので導入します。 |
| [PhantomJS](https://github.com/ariya/phantomjs)<br>1.9.8 | ブラウザー (ヘッドレス)<br><br>Karma でコードを動かすためのブラウザーは Chrome を筆頭に選択可能ですが、ヘッドレスな方が都合がよかったので PhantomJS を使うことにしました。 |
| [gulp](https://github.com/gulpjs/gulp)<br>3.9.0 | タスクランナー |

また、テストコードを含めた JavaScript のコードを ES6 で書くために、 [Babel](https://github.com/babel/babel) や [browserify](https://github.com/substack/node-browserify) も利用します。

# <i class="fa fa-fw fa-sort-numeric-asc"></i>手順

## 01. npm パッケージのインストール

* gulp や Babel, browserify は既に導入していることを前提とし、ここでは Karma で利用するパッケージをインストールします。

```
$ npm install --save-dev karma karma-babel-preprocessor karma-browserify \
karma-jasmine karma-jasmine-jquery \
karma-phantomjs-launcher karma-spec-reporter
```

## 02. Karma の設定ファイル作成

Karma の設定ファイルを作成します。`karma init` コマンドを使って対話形式で作成することも可能です。
以下の設定ファイルは `spec` ディレクトリ配下にテストコードを配置することを想定しています。
また、これは個人的な好みですが、テスト結果を出力するためのレポーターに [karma-spec-reporter](https://github.com/mlex/karma-spec-reporter) を使っています。

```js:karma.conf.js
module.exports = function (config) {
  config.set({
    frameworks: ['browserify', 'jasmine-jquery', 'jasmine'],
    browsers: ['PhantomJS'],
    files: [
      'spec/**/*.js'
    ],
    preprocessors: {
      'spec/**/*.js': ['browserify']
    },
    browserify: {
      debug: true,
      transform: ['babelify']
    },
    reporters: ['spec']
  });
};
```

## 03. gulp タスクの作成

gulpfile にテストを実行するためのタスクを追加します。
以下の gulpfile は ES6 で記述しています (そのためファイル名を `gulpfile.babel.js` としています) 。

```js:gulpfile.babel.js
import gulp from 'gulp';
import { Server } from 'karma';

gulp.task('spec', (done) => {
  new Server({
    configFile: `${__dirname}/karma.conf.js`,
    singleRun: true
  }, done).start();
});
```

[gulp-karma](https://github.com/karma-runner/gulp-karma) という npm パッケージが存在したのでこれを利用しようと思ったのですが、README.md に

> You don't need any gulp plugins (why?) to run Karma from the Gulp-based build, use Karma directly

と書かれていたのでそれに従いました。

以上で単体テストを実行するための環境は揃いました。

## 04. テストの実装

環境の動作を確認するために、簡単なコードとそのテストコードを用意します。

```app/views/hidamari.js
import { View } from 'backbone';
import _ from 'underscore';

export default View.extend({
  template: _.template("<a class='hidamari' href='http://www.tbs.co.jp/anime/hidamari/'>ひだまりスケッチ</h1>"),
  events: {
    'click .hidamari': 'onClickHidamari'
  },

  render() {
    this.$el.html(this.template());
    return this;
  },

  onClickHidamari() {
    this.$('.hidamari').after('<p>ひだまり荘で、待ってます。</p>');
  }
});
```

```spec/views/hidamari_spec.js
import HidamariView from '../../app/views/hidamari';

describe('HidamariView', () => {
  const view = new HidamariView();

  describe('#render', () => {
    beforeEach(() => {
      view.render();
    });

    it('.hidamari を含むこと', () => {
      expect(view.$el).toContainElement('.hidamari');
    });

    it('"ひだまりスケッチ" を含むこと', () => {
      expect(view.$el).toContainText('ひだまりスケッチ');
    });
  });

  describe('events', () => {
    beforeEach(() => {
      spyOn(view, 'onClickHidamari').and.callThrough();
      view.delegateEvents();
      view.render();
      view.$('.hidamari').click();
    });

    it('onClickHidamari が呼ばれること', () => {
      expect(view.onClickHidamari).toHaveBeenCalled();
    });

    it('"ひだまり荘で、待ってます。" を含むこと', () => {
      expect(view.$el).toContainText('ひだまり荘で、待ってます。');
    });
  });
});
```

## 05. 単体テストの実行

手順 03 で作成した gulp タスクを実行します。

```
$ gulp spec
[17:27:22] Requiring external module babel-core/register
[17:27:22] Using gulpfile ~/workspace/hidamari/gulpfile.babel.js
[17:27:23] Starting 'spec'...
12 12 2015 17:27:24.945:INFO [framework.browserify]: bundle built
12 12 2015 17:27:24.951:INFO [karma]: Karma v0.13.15 server started at http://localhost:9876/
12 12 2015 17:27:24.960:INFO [launcher]: Starting browser PhantomJS
12 12 2015 17:27:26.592:INFO [PhantomJS 1.9.8 (Mac OS X 0.0.0)]: Connected on socket _6cuZesiQyb16Mg4AAAA with id 7039758

  HidamariView
    #render
      ✓ .hidamari を含むこと
      ✓ "ひだまりスケッチ" を含むこと
    events
      ✓ onClickHidamari が呼ばれること
      ✓ "ひだまり荘で、待ってます。" を含むこと

PhantomJS 1.9.8 (Mac OS X 0.0.0): Executed 4 of 4 SUCCESS (0.002 secs / 0.009 secs)
TOTAL: 4 SUCCESS

[17:27:26] Finished 'spec' after 3.61 s
```

無事に動くことを確認できました！ ＼＼\\٩( 'ω' )و//／／