踊る犬.netブログ (旧)

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

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

Webpackとは

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

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

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

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

使用するツール群

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

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

テストの実施準備手順

設定項目や手順の意味を理解するために、敢えてバラバラに分けて説明します。
筆者は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)