Karma＋mocha＋power-assertのセットアップ


# はじめに

以前、JavaScriptのユニットテスト環境を、Testem（テストランナー）＋mocha（テスティングフレームワーク）＋chai（アサーションライブラリ）という構成にしていました。

この構成でも特に問題はなかったのですが、[Karma for JavaScript test runner - blog.koba04.com](http://blog.koba04.com/post/2014/06/23/karma-for-javascript-test-runner/)を拝見して「Karma良さそう…！」と思い、今はKarma＋mocha＋power-assert（タスクランナーはGrunt）という構成で落ち着いています。
（ちなみに、power-assertはシンプルなアサーションと、テスト失敗時の詳細かつ直感的なログ出力がとても素敵です＞＜）

この記事は、Karma＋mocha＋power-assertの構成をどのように構築したかの備忘録です。Node.jsやGrunt、Karmaなどのインストール手順は省略しております。

# 目的

この構成で実現したいユニットテストの方法としては、テスト対象のjsファイル、もしくはテストコードを記述したjsファイルを変更した際に、Chrome上で自動的にテストを実行したいというものです。

なお、アサーションライブラリにpower-assertを利用する場合は、power-assert用にコードを変換し、その変換後のコードに対してテストを実行する必要があります。
（このあたりの仕様は[power-assertのGithub](https://github.com/twada/power-assert)を参照のこと）

# フォルダ構成

今回の記事が対象としているフォルダ構成は下記の通りです。

```
Root
│  Gruntfile.js
│  karma.conf.js
│  package.json
│  index.html
│
├─css
│    index.css
│
├─js
│  │  index.js
│  │
│  └─vendor
│          jquery-2.1.0.js
│
│
└─test
   │  index_test.js
   │
   └─vendor
           power-assert.js

```

# 必要なGruntプラグインのインストール
## grunt-este-watch
ファイルの変更を検知するプラグイン

```
npm install grunt-este-watch --save-dev
```

## grunt-karma
KarmaをGruntから実行するためのプラグイン

```
npm install grunt-karma --save-dev
```
## grunt-espower
espower（power-assert用にコードを変換するツール）のプラグイン

```
npm install grunt-espower --save-dev
```


# Gruntfile.js

Gruntfile.jsの設定内容は下記の通りです。

ここで定義している処理の流れは

1. js/index.jsもしくはtest/index_test.jsが変更・保存されたのを検知する
2. index_test.jsをpower-assert用に変換する
3. 2. の変換後のソース（test/espowered/*.js）を対象にテストを実行する

です。

```javascript:Gruntfile.js
module.exports = function (grunt) {
    'use strict';

    var pkg = grunt.file.readJSON('package.json');

    grunt.initConfig({
        pkg: pkg,
        // power-assert用にコードを変換するタスク
        espower: {
            test: {
                files: [
                    {
                        expand: true, 
                        cwd: 'test/',             // 変換対象のファイルを配置しているディレクトリ
                        src: ['*.js'],            // 変換対象のファイル名
                        dest: 'test/espowered/',  // 変換後のコードを配置するディレクトリ
                        ext: '.js'                // 変換後のファイルの拡張子
                    }
                ]
            }
        },
        // ファイルの変更検知用タスク
        esteWatch: {
            options: {
                dirs: ['js', 'test'],
                livereload: false
            },
            js: function (filepath) {
                return ['test'];
            }
        },
        // Karma実行用タスク
        karma: {
            unit: {
                options: {
                    configFile: 'karma.conf.js',
                    autoWatch: true,
                    browsers: ["Chrome"],
                    reporters: ["progress"],
                    singleRun: true,
                    keepalive: true
                }
            }
        }
    });

    // package.json内のdevDependenciesで定義されているパッケージ中『grunt-』で始まるものをロードする
    for (var taskName in pkg.devDependencies) {
        if (taskName.substr(0, 6) == 'grunt-') {
            console.log('load npm task -> ' + taskName);
            grunt.loadNpmTasks(taskName);
        }
    }

    grunt.registerTask("default", "esteWatch");
    grunt.registerTask('test', ['espower', 'karma']);
};
```

# karma.conf.js

karma.conf.jsの設定内容は下記の通りです。

```javascript:karma.conf.js
module.exports = function (config) {
    config.set({

        // base path that will be used to resolve all patterns (eg. files, exclude)
        basePath: '',


        // frameworks to use
        // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
        frameworks: ['mocha', 'mocha-debug'],


        // list of files / patterns to load in the browser
        files: [
            'js/vendor/jquery-2.1.0.js',
            'js/*.js',
            'test/vendor/power-assert.js',
            'test/espowered/*.js'
        ],


        // list of files to exclude
        exclude: [

        ],


        // preprocess matching files before serving them to the browser
        // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
        preprocessors: {

        },


        // test results reporter to use
        // possible values: 'dots', 'progress'
        // available reporters: https://npmjs.org/browse/keyword/karma-reporter
        reporters: ['progress'],


        // web server port
        port: 9876,


        // enable / disable colors in the output (reporters and logs)
        colors: true,


        // level of logging
        // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
        logLevel: config.LOG_INFO,


        // enable / disable watching file and executing tests whenever any file changes
        autoWatch: false,


        // start these browsers
        // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
        browsers: ['Chrome'],


        // Continuous Integration mode
        // if true, Karma captures browsers, runs the tests and exits
        singleRun: true
    });
};
```
# 実行方法
プロジェクトのルートディレクトリに移動し、コマンドラインから下記を実行してください。

```
$ grunt
```

gruntコマンドを引数なしで実行しているため、Gruntfile.jsで定義しているデフォルトのタスク（『grunt.registerTask("default", "esteWatch");』）が実行されます。

esteWatchはテスト対象のコード（js/index.js）およびテストコード（test/index_test.js）の変更を監視しています。
これらのファイルに変更があれば、前述の処理の流れでテストを実行します。

# 実行結果

### ファイル変更・保存時→テスト失敗

テスト失敗時の例として、こういったサンプルにありがちな、Greetingオブジェクトのgreet関数のテストを実行してみたいと思います。

#### テスト対象のコード（js/index.js）

```javascript:js/index.js
window.ns = {};

(function (ns) {
    'use strict';

    var Greeting = ns.Greeting = function (name) {
        this.name = name;
    };

    Greeting.prototype.greet = function () {
        return 'Hello, ' + this.name + '!!';
    };
}(ns));
```

#### テストコード（test/index_test.js）

```javascript:test/index_test.js
describe('Greeting', function () {
    describe('#greet()', function () {
        it('Greetingをnewする時にnameを渡さなかった場合', function () {
            var greeting = new ns.Greeting();
            assert(greeting.greet() === 'Hello, unknown!!');
        });
    });
});
```

test/index_test.jsか、js/index.jsを変更・保存すると、下記の通りテストが実行されます。

```shell-session
Running "esteWatch" task
>> Waiting...
>> User action.
>> File changed: test/index_test.js

Running "espower:test" (espower) task

Running "karma:unit" (karma) task
INFO [karma]: Karma v0.12.19 server started at http://localhost:9876/
INFO [launcher]: Starting browser Chrome
INFO [Chrome 36.0.1985 (Mac OS X 10.9.2)]: Connected on socket hklYkb1RNy6QzmNzoXL1 with id 78195425
Chrome 36.0.1985 (Mac OS X 10.9.2) Greeting #greet() Greetingをnewする時にnameを渡さなかった場合 FAILED
        # test/index_test.js:5

        assert(greeting.greet() === 'Hello, unknown!!')
               |        |       |                      
               |        |       false                  
               |        "Hello, undefined!!"           
               Object{name:undefined}                  

        --- [string] 'Hello, unknown!!'
        +++ [string] greeting.greet()
        @@ -6,11 +6,13 @@
         , un
        -known
        +defined
         !!
```

### ファイル変更・保存時→テスト成功

テストが通るようにテスト対象のコード（js/index.js）を変更・保存すると、下記の通りテストが実行されます。

```shell-session
Running "esteWatch" task
>> Waiting...
>> User action.
>> File changed: test/index_test.js

Running "espower:test" (espower) task

Running "karma:unit" (karma) task
INFO [karma]: Karma v0.12.19 server started at http://localhost:9876/
INFO [launcher]: Starting browser Chrome
INFO [Chrome 36.0.1985 (Mac OS X 10.9.2)]: Connected on socket b1JTt49h-waXPjMA6Jb2 with id 87727203
Chrome 36.0.1985 (Mac OS X 10.9.2): Executed 3 of 3 SUCCESS (0.129 secs / 0.003 secs)

Running "esteWatch" task
>> Waiting...

```

これでテストがさくさく書けますね！