grunt+istanbul+mochaでNode.jsのテスト&カバレッジ計測を行う


単体テストの件数や可否ももちろんですが、コードのカバー率も可視化されることで、品質向上の1つの指標にもなります。
Node.jsで開発しているプロジェクトについて、こういったデータをgruntタスクで簡単に生成できるようにしましょう。

仕組みとして使うものは以下のとおりです。

  • grunt: JSタスクランナー
  • mocha: JSテストフレームワーク
  • chai: BDD/TDDアサーションライブラリ
  • sinon: Spy,Stub,Mockライブラリ
  • istanbul: コードカバレッジ計測ツール

gruntは既に導入済みで、活用している前提とします。
JSのテストはJasmineが有名ですが、自由度の高いmocha+chai+sinonが個人的にはお気に入りなのでこちらを使います。
コードカバレッジ計測はいくつかの選択肢がありますが、メソッドや行、分岐等を計測できるistanbulを使います。

流れとしては、

  • 関連パッケージをインストールする
  • Gruntfileを設定する
  • mocha+chai+sinonでテストを書く
  • gruntタスクからistanbul経由でmochaを実行、コードカバレッジ情報を出力する

となります。

関連パッケージをインストールする

まずは、mochaとistanbulのインストール。
grunt-mocha-istanbulというgruntタスクのパッケージがあるのでこちらを使います。

npm install --save-dev grunt-mocha-istanbul

続いて、chai+sinonのインストール。
こちらは、sinon-chaiというパッケージを入れます。
これにより、Sinon.JSのspy,stub,mock系メソッドががchaiのassertionとして使えるようになります。

npm install --save-dev sinon-chai

後述しますがCoffeeScriptでテストを書きたいので、grunt-contrib-coffeegrunt-coffee-jshintも入れておきましょう。

npm install --save-dev grunt-contrib-coffee grunt-coffee-jshint

Gruntfileを設定する

istanbulはCoffeeScriptを直接解釈できないため、インストールしたgrunt-contrib-coffeeやgrunt-coffee-jshintを利用して、

  • CoffeeScriptでコードを書く
  • jsHint後CoffeeScript->JavaScript変換(ビルド)
  • ビルド後のコードでテスト実施

となるようにGruntfile.coffeeを設定します。

ibrikというistanbulのCoffeeScript版を使う手もありますが、mocha連携等が上手く行きませんでした。

coffee_jshint設定

test以下のコードに対してjsHintによるチェックをかける設定です。
残念ながらmocha、chai、sinonに関してはjsHintのオプションが無いため、globalsに使用するものを列挙しておきます。
ついでにGruntfile.coffeeのチェックも仕掛けておきます。

Gruntfile.coffee
    coffee_jshint:
      gruntfile:
        options:
          jshintOptions: [ 'node' ]
        src: 'Gruntfile.coffee'
      test:
        options:
          jshintOptions: [ 'node' ]
          globals: [ 'after', 'afterEach', 'before', 'beforeEach', 'describe', 'it', 'expect', 'sinon' ]
        src: 'test/**/*.coffee'

coffee設定

CoffeeScript->JavaScript変換の設定です。
test以下のファイルを変換し、build/test以下に出力します。
ファイル名に「.」(ドット)が含まれると誤動作する問題があるので、renameを独自に定義しておきます。

Gruntfile.coffee
    coffee:
      test:
        options:
          bare: true
        expand: true
        cwd: 'test'
        src: '**/*.coffee'
        dest: 'build/test'
        rename: (dest, src) ->
          dirname  = src.replace(/[^\/]*$/, '')
          basename = src.replace(/.*\//, '').replace(/\.[^.]*$/, '')
          "#{dest}#{dirname}/#{basename}.js"

mocha_istanbul設定

テストとコードカバレッジ計測の設定です。
build/test以下の*.spec.jsをテストとして実行し、lcovフォーマットで出力するよう設定します。

Gruntfile.coffee
    mocha_istanbul:
      test:
        src: 'build/test'
        options:
          mask: '**/*.spec.js'
          reportFormats: [ 'lcov' ]

testタスクを作成

grunt.registerTaskでtestタスクを作成し、一連のタスクが実行されるようにします。

Gruntfile.coffee
  grunt.registerTask 'test', 'run test and generate coverage information', [
    'coffee_jshint:test'
    'coffee:test'
    'mocha_istanbul:test'
  ]

grunt-contrib-watchを使用しているのであれば、以下の様なwatchタスクを作成してもいいかもしれません。

Gruntfile.coffee
    watch:
      gruntfile:
        files: '<%= coffee_jshint.gruntfile.src %>'
        tasks: 'coffee_jshint:gruntfile'
      test:
        files: '<%= coffee_jshint.test.src %>'
        tasks: 'test'

mocha+chai+sinonでテストを書く

単体テストの書き方は調べれば沢山出てきますので、ここでは割愛します。
mocha+chai+sinonでテストコードを書く場合は、個人的にはCoffeeScriptで書くことをオススメします。
余計な括弧がなくなることで、テストのコードがかなり見やすくなります。

例えば、以下のようなイメージでtestディレクトリ以下にテストコードを設置します。
テスト対象を用意するのが面倒なので、とりあえずunderscore.jsを対象として書いてみたサンプルです。

test/underscore.spec.coffee
chai = require 'chai'
expect = chai.expect
sinon = require 'sinon'
chai.use require('sinon-chai')

_ = require '../src/underscore'

describe 'Underscore', ->
  describe 'main', ->
    it 'should be a function', ->
      expect(_).to.be.a 'function'

  describe '#bind', ->
    it 'should be a function', ->
      expect(_.bind).to.be.a 'function'

    it 'should return a bound function', ->
      func = sinon.spy()
      obj = {}
      newfunc = _.bind func, obj, 'sample'
      expect(newfunc).to.be.a 'function'
      expect(func).to.have.not.been.called
      newfunc()
      expect(func).to.have.been.calledOnce
      expect(func).to.always.have.been.calledOn obj
      expect(func.firstCall.args[0]).to.equal 'sample'

最後に、underscore.jsをbuild/src以下に設置しておきます。
本来ならsrc/*.coffee等を作成してbuild/src以下にビルドする感じになりますが、とりあえずということで。

mkdir -p build/src
curl -o build/src/underscore.js http://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.6.0/underscore.js

gruntでtestタスクを実行してみる

grunt testを実行して、以下の様な結果が表示されればOKです。

coverageディレクトリが生成されていますので、その中のlcov-report/index.htmlを開くと、coverage情報をブラウザ上で確認することができます。
通過した部分が緑、未通過の部分が赤で表示されていることがわかります。

Node.jsはFunctionオブジェクトにnativeでbindメソッドを持っているため、612行目でreturnしてしまっています。

全てを必ず100%にする必要は無いですし、カバレッジを100%にしたとしてもテストコードが「通過した」というだけに過ぎず、必ずしもコードの品質が上がるわけではありません。
しかしながら、カバレッジデータを見ながらテストコードを書くことで、未検証のコードが明らかになり、効率よくテストコードを書いていくことができるようになるのは非常に大きいと思いますので、是非やってみましょう。