WebpackベースのアプリケーションをKarmaでテストする

banner

Webpackベースのアプリケーションをテストしたい

Webpackとは

Webpackはモジュールの依存関係を解決するプリプロセッサです。
依存関係の記述には、ES6、CommonJSやAMDのスタイルが使えます。
誤解を恐れずに言えば、node.js感覚で他のモジュールをrequireできるようにしてくれます。
webpackでソースコードをコンパイルすると、*.bundle.jsというような名前のファイルが出力されます。
これをアプリケーション側で事で、同期的・非同期的なモジュールの依存関係の解決が可能となります。

テストしやすい方法の検討

要件としては以下のとおりです:

  1. Webpackで書いたアプリケーションの各ユニットあるいは各ふるまいを、出来るだけ簡単にテストしたい
  2. 任意のCI(Continuous Integration)でも動かしたい

使用するツール群

前述の要件を満たすツールの組み合わせがたくさんあります。
今回は以下の構成でテストを走らせてみます:

  • Karma – テストランナー
  • PhantomJS – ヘッドレスブラウザ
  • Mocha – テストフレームワーク
  • Chai – アサーション
  • webpack – プリプロセッサ
  • bower – パッケージ管理

各ツールの詳しい説明は、それぞれの本家をご参照ください。

テストの実施準備手順

設定項目や手順の意味を理解するために、敢えてバラバラに分けて説明します。
筆者はMac OS Xの環境で行いました。

必要なパッケージのインストール

$ npm install --save-dev 
    karma 
    karma-chai 
    karma-mocha 
    karma-phantomjs-launcher 
    karma-webpack

Karmaの設定

karma.conf.jsで以下のファイルを作成します。

// Karma configuration
// Generated on Wed Nov 26 2014 23:12:23 GMT+0900 (JST)

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', 'chai'],


    // list of files / patterns to load in the browser
    files: [
      'test/**/*_test.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: {
      // add webpack as preprocessor
      'test/**/*_test.js': ['webpack', 'sourcemap']
    },

    // 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: true,


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


    // Continuous Integration mode
    // if true, Karma captures browsers, runs the tests and exits
    singleRun: false,

    // karma watches the test entry points
    // (you don't need to specify the entry option)
    // webpack watches dependencies
    // webpack configuration
    webpack: {
    }

  });
};

Bowerのモジュールを読み込めるようにする

設定ファイル冒頭に以下を追記します:

var webpack = require("webpack");
var path = require("path");
var bowerPath = path.join( __dirname, "bower_components" );

webpackの設定に以下を追記します(参考: usage with bower)

    webpack: {
      resolve: {
        // Tell webpack to look in node_modules, then bower_components when resolving dependencies
        // If your bower component has a package.json file, this is all you need.
        modulesDirectories: ["node_modules", bowerPath]
      },
      // Define a new plugin that tells webpack to look at the main property in bower.json files when resolving dependencies.
      // For marionette, we need it to load the CJS version, which we specify with as ["main", "1"] in the args below.
      plugins: [
        new webpack.ResolverPlugin([
          new webpack.ResolverPlugin.DirectoryDescriptionFilePlugin( "bower.json", ["main", ["main", "1"]] )
        ], ["normal", "loader"])
      ]
    }

これで、テストのソースコードからbowerのモジュールをrequire("jquery");というように記述して読み込めるようになりました。

テスト対象のサンプルモジュール

簡単なjqueryを用いるサンプルモジュールです。
index.js で以下のようなファイルを用意してください。

var $ = require('jquery');

module.exports = function() {
  var div = $('<div />');
  $('body').append(div);
  return div;
};

依存関係にある jquery をbowerでインストールしておきます:

$ bower install --save jquery

テストの用意

test/main_test.js で以下のようなファイルを作成します:

var createElement = require('../index.js');

describe('jQuery sample', function() {

  it('should create div element', function() {
    expect(createElement).to.be.Function;
    var div = createElement();
    expect(div).to.be.Array;
  });

});

処理内容としては、先ほど作成したサンプル関数(createElement)を読み込みます。
次にその関数を呼び出し、結果を検査しています。

テストの実行

以下のコマンドを実行します:

$ karma start

結果:

$ karma start
INFO [karma]: Karma v0.12.28 server started at http://localhost:9876/
INFO [launcher]: Starting browser PhantomJS
Hash: 606dfda4c9d20a78987a
Version: webpack 1.4.13
Time: 8ms
webpack: bundle is now VALID.
webpack: bundle is now INVALID.
Hash: 1b62ba3e4c6b38edee7e
Version: webpack 1.4.13
Time: 944ms
                Asset    Size  Chunks             Chunk Names
_js/test/main_test.js  693312       0  [emitted]  test/main_test.js
chunk    {0} _js/test/main_test.js (test/main_test.js) 247722 [rendered]
    [0] ./test/main_test.js 245 {0} [built]
    [1] ./index.js 126 {0} [built]
    [2] ./bower_components/jquery/dist/jquery.js 247351 {0} [built]
webpack: bundle is now VALID.
INFO [PhantomJS 1.9.8 (Mac OS X)]: Connected on socket Mfny5tbQCtBeW9kEu0TR with id 80467111
PhantomJS 1.9.8 (Mac OS X): Executed 1 of 1 SUCCESS (0.011 secs / 0.002 secs)

動きましたね!

さらに改善する

ソースマップ

Webpackでコンパイルしたテストがコケた時、示される行番号が参考にできません。
ソースマップを有効にすることで、コンパイル前の行番号を取得できます。

$ npm install --save-dev karma-sourcemap-loader

preprocessorsに加えます:

preprocessors: {
    'test/test_index.js': ['webpack', 'sourcemap']
}

また、webpackにソースマップを生成するよう設定します:

webpack: {
  // ...
    devtool: 'inline-source-map'
}

実行すると以下のように元の位置が併せて示されます:

PhantomJS 1.9.8 (Mac OS X) Hello test Is it right? FAILED
        TypeError: '[object Object]' is not a function (evaluating 'should(T)')
            at /Users/nora/Development/tmp/karma-test2/test/main_test.js:53:0 <- webpack:///./test/main_test.js:7:0
PhantomJS 1.9.8 (Mac OS X): Executed 1 of 1 (1 FAILED) ERROR (0.006 secs / 0 secs)

CoffeeScript(1.7+)で書いたテストをmochaで実行する

mocha

メモ。
検索しても古いやりかたしか出てこなかったので。

mochaはnode.js製のテストフレームワークです。
CoffeeScriptはJavaScriptのトランスパイラです。

CoffeeScriptで書いたテストをmochaで実行できるのですが ドキュメント によると、 CoffeeScript 1.7からそのコマンドが変わっています。

coffee-script is no longer supported out of the box. CS and similar transpilers may be used by mapping the file extensions (for use with –watch) and the module name. For example –compilers coffee:coffee-script with CoffeeScript 1.6- or –compilers coffee:coffee-script/register with CoffeeScript 1.7+.

というわけで、以下のように実行します:

$ mocha --compilers coffee:coffee-script/register

古いやり方で出るエラー

$ mocha --compilers coffee:coffee-script
/.../node_modules/coffee-script/lib/coffee-script/coffee-script.js:195
          throw new Error("Use CoffeeScript.register() or require the coffee-s
                ^
Error: Use CoffeeScript.register() or require the coffee-script/register module to require .coffee.md files.
  at Object._base.(anonymous function) [as .coffee] (/.../node_modules/coffee-script/lib/coffee-script/coffee-script.js:195:17)
  at Module.load (module.js:355:32)
  at Function.Module._load (module.js:310:12)
  at Module.require (module.js:365:17)
  at require (module.js:384:17)
  at /.../node_modules/mocha/lib/mocha.js:184:27
  at Array.forEach (native)
  at Mocha.loadFiles (/.../node_modules/mocha/lib/mocha.js:181:14)
  at Mocha.run (/.../node_modules/mocha/lib/mocha.js:393:31)
  at Object.<anonymous> (/.../node_modules/mocha/bin/_mocha:380:16)
  at Module._compile (module.js:460:26)
  at Object.Module._extensions..js (module.js:478:10)
  at Module.load (module.js:355:32)
  at Function.Module._load (module.js:310:12)
  at Function.Module.runMain (module.js:501:10)
  at startup (node.js:124:16)
  at node.js:842:3