webpack + Testem でフロントエンド JavaScript のテストを書く

webpack を使っているプロジェクトで、テストコードも webpack で依存関係の解決やトランスパイルをしたいということがあったので、その時に行ったことをまとめます。

webpack の設定

テストコードは個々のファイルがエントリポイントとなっているため、webpack の設定でもそのように指定する必要があります。 entry にエントリポイントにしたいファイルを配列で渡すことで、ロード時に指定されたファイルをすべて実行するようにビルドされます。webpack が標準で glob パターンなどをサポートしてないので、別途自分で書く必要があります。

// webpack.config.test.js
const path = require('path');
const glob = require('glob');

module.exports = {
  // ...
  entry: glob.sync('./test/**/*.js'),
  output: {
    path: path.resolve(__dirname, '.tmp'),
    filename: 'test.js'
  },
  // ...
};

Testem の設定

Testem には webpack で出力されたファイルを読み込ませます。 src_files パスを指定するだけで良いです。

# testem.yml
---
  framework: mocha
  src_files:
    - .tmp/test.js

watch させるスクリプトを書く

テストコードが変更される度にテストの実行を行いたい場合、別途スクリプトを書く必要があります。Node で書く場合、以下のように、 Testem.prototype.startDev を実行することで Testem を起動させることができます (たぶんドキュメントに書かれてない)。gulp を使っているプロジェクトなら、 webpack と Testem を watch させるタスクを書くのが一番簡単だと思います。

// gulpfile.js
const fs = require('fs');
const webpack = require('webpack');
const Testem = require('testem');
const yaml = require('js-yaml');

gulp.task('webpack:test', () => {
  const compiler = webpack(require('...config_file...'));

  compiler.watch(200, (err) => {
    if (err) throw new err;
  });
});

gulp.task('testem', () => {
  const testem = new Testem();
  testem.startDev(yaml.safeLoad(fs.readFileSync(__dirname + '/testem.yml')));
});

gulp.task('test', ['webpack:test', 'testem']);

シェルスクリプトで書く場合は以下のようになります。単純に & で複数実行するだけだと、 ctrl-C した時にバックグラウンドタスクが終了しないため、終了させるための処理を書く必要があります。 trap 'kill %1' SIGINTctrl-C された時にバックグラウンドタスクにまわした webpack を終了してくれるようになります。また、Testem を終了させた時、webpack を終了させるまで待つために、 wait コマンドの実行が必要です。

#!/bin/bash

trap 'kill %1' SIGINT
webpack --watch --config (config_file) & testem
wait

テストコードを書く

ここまでやれば後はテストコードを書くだけです。webpack がモジュール読み込みやトランスパイルなどをやってくれるため、ブラウザ上で動かす必要のあるテストも書きやすいと思います。

import assert from 'power-assert';
import module from '../src/some-module';

describe('Module', () => {
  it('should return true', () => {
    assert(module() === true);
  });

  ...
});

おわりに

個人的に、テストコードを書くことは二の次になりやすいので、テストを行うまでのプロセスを簡単にすることや、テストコードを書くこと自体を楽にすることは重要視しています。テストコードを webpack でビルドできるようにしたり、変更を watch させたりすることで、テストコードをとても書きやすくなったと感じてます。

Vue.js における methods の this は自動的に VM に束縛される

執筆当時の環境

  • Vue.js v1.0.17

JavaScript で以下の様なコードを書いた時、onResize 内の this はグローバルオブジェクト (window) となり、this.log('resized') はエラーとなります。

const obj = {
  log: function(str) {
    console.log(str);
  },

  onResize: function(event) {
    this.log('resized');
  }
};

window.addEventListener('resize', obj.onResize); // this.log('resized') でエラー

上記のコードの obj を、以下のように Vue.js の VM にするとエラーが起きなくなり、意図した通りに動作するようになります。また、this の値は obj に束縛されています。

const obj = new Vue({
  methods: {
    log: function(str) {
      console.log(str);
    },

    onResize: function(event) {
      this.log('resized');
    }
  }
});

window.addEventListener('resize', obj.onResize); // エラーが発生しない

このことから、Vue.js では methods に渡された関数を VM に束縛していると言えます。実際にコードを追って確かめてみます。

methods で Github のリポジトリを検索すると、 _initMethods というメソッドが存在しているのがわかります。このメソッドは src/instance/internal/state.js の中に定義されています。_initMethods の中を見てみると、bind(methods[key], this) という記述があります。JavaScript ネイティブの bind ではないですが、どうやらここで this を束縛しているように見えます。

vue/state.js at 521e8d2754c2e7f172c3c9702fdb74fe993027fb · vuejs/vue

なぜわざわざ独自の bind 関数を使っているのかを調べるために、この bind の定義も見てみました。bindsrc/util/lang.js に定義されています。

vue/lang.js at 521e8d2754c2e7f172c3c9702fdb74fe993027fb · vuejs/vue

関数の上のコメントには、ネイティブの bind よりも早いと書いてあります。少し調べてみたところネイティブの bind は、this の束縛の他に型チェックや、引数の束縛なども行うため遅くなっているとのことでした。ただ、遅いとはいってもほとんどのケースでは気にならない違いだと思うので、普通にアプリケーションを作る際には気にしなくても良さそうです。

javascript – Why is bind slower than a closure? – Stack Overflow