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)

npmを宇宙に見立ててパッケージと依存関係をビジュアライズする「npm universe」

npm-university-1

npm-university-2

npm名前空間を宇宙、パッケージを星として、依存関係を線で繋ぎ描写しています。
まるで宇宙にインクをこぼしたかのような印象。
有名なパッケージ名で検索すると面白いですよ!

操作方法

  • WASDキーで移動
  • Lキーで依存関係の表示切り替え
  • Spaceキーでステアリングモードの切り替え

参考

chef-clientが処理の途中で403 Forbiddenエラーになる時の対処

Chef

認証に問題が無いのに404 Forbiddenになる

chef-clientを走らせていて、以下のようなエラーにたまに出くわします:

198.xx.xxx.xxx ================================================================================
198.xx.xxx.xxx Error executing action `create` on resource 'template[/var/www/mysite.com/shared/config/database.yml]'
198.xx.xxx.xxx ================================================================================
198.xx.xxx.xxx
198.xx.xxx.xxx Net::HTTPServerException
198.xx.xxx.xxx ------------------------
198.xx.xxx.xxx
198.xx.xxx.xxx 403 "Forbidden"
198.xx.xxx.xxx
198.xx.xxx.xxx
198.xx.xxx.xxx Resource Declaration:
198.xx.xxx.xxx
198.xx.xxx.xxx ---------------------
198.xx.xxx.xxx # In /var/chef/cache/cookbooks/rails/recipes/production.rb
198.xx.xxx.xxx
198.xx.xxx.xxx  40: template node[:rails][:app_root]+"/shared/config/database.yml" do
198.xx.xxx.xxx  41:     owner "root"
198.xx.xxx.xxx  42:     group "xx-dev"
198.xx.xxx.xxx  43:     mode 0775
198.xx.xxx.xxx  44: end
198.xx.xxx.xxx  45:
198.xx.xxx.xxx
198.xx.xxx.xxx Compiled Resource:
198.xx.xxx.xxx
198.xx.xxx.xxx ------------------
198.xx.xxx.xxx
198.xx.xxx.xxx # Declared in /var/chef/cache/cookbooks/rails/recipes/production.rb:40:in `from_file'
198.xx.xxx.xxx
198.xx.xxx.xxx template("/var/www/mysite.com/shared/config/database.yml") do
198.xx.xxx.xxx   provider Chef::Provider::Template
198.xx.xxx.xxx   action "create"
198.xx.xxx.xxx   retries 0
198.xx.xxx.xxx   retry_delay 2
198.xx.xxx.xxx   path "/var/www/mysite.com/shared/config/database.yml"
198.xx.xxx.xxx   backup 5
198.xx.xxx.xxx   source "database.yml.erb"
198.xx.xxx.xxx   cookbook_name "rails"
198.xx.xxx.xxx   recipe_name "production"
198.xx.xxx.xxx   mode 509
198.xx.xxx.xxx   owner "root"
198.xx.xxx.xxx   group "xx-dev"
198.xx.xxx.xxx end
198.xx.xxx.xxx
198.xx.xxx.xxx [2014-05-29T20:16:34+00:00] ERROR: Running exception handlers
198.xx.xxx.xxx [2014-05-29T20:16:34+00:00] FATAL: Saving node information to /var/chef/cache/failed-run-data.json
198.xx.xxx.xxx [2014-05-29T20:16:34+00:00] ERROR: Exception handlers complete
198.xx.xxx.xxx [2014-05-29T20:16:34+00:00] FATAL: Stacktrace dumped to /var/chef/cache/chef-stacktrace.out
198.xx.xxx.xxx [2014-05-29T20:16:34+00:00] FATAL: Net::HTTPServerException: template[/var/www/mysite.com/shared/config/database.yml] (rails::production line 40) had an error: Net::HTTPServerException: 403 "Forbidden"

しかし、最初はうまく走っていたので、認証周りで何かおかしいとは考え難い現象です。

no_lazy_loadを指定してみる

もしあなたのchef-clientが、十数分とか長い時間動作しているとしたら、以下のフラグをclient.rbに追記すると改善するかもしれません:

no_lazy_load true

chef-clientは必要に応じてcookbook_filetemplateを取得します(lazy-load)。
しかし、長い時間走らせていると、認証がタイムアウトするようです。
これを防ぐため、no_lazy_load trueを指定することでlazy-loadを無効にします。
これによって、はじめに必要なデータをすべて取得するようになります。

参考

[Mac OSX] VPNが頻繁に切断されてしまう時の対処方法

OpenVPN-Logo-200x200

社内へのアクセスにVPNを利用している人は多いと思います。
Mac OSXは標準でPPTPやL2TPなどのプロトコルに対応しています。
しかしながら、設定内容は合っているのに接続直後に切断されてしまったり安定しないケースがしばしばあります。
そんな時の対処方法をまとめました。
以下の方法についてそれぞれ説明します。

  1. MTU値を調整する
  2. PPTPマルチパススルー機能のついたルータを買う
  3. 自動再接続スクリプトを書く

MTU値を調整する

個人的にはこの方法が最も有効です。
MTU(Maximum Transmission Unit)とは、一度にネットワークへ送信できる最大のパケットサイズの事です。
大きいほど一度に沢山のデータを送信できますが、エラー率も上がり再送信コストが大きいというトレードオフがある設定値です。
これを調整する事で、切断の頻度を改善できます。

現在のMTU値を確認

まずは、現在のMTU値を確認しましょう。
「ターミナル.app (Terminal.app)」を開いて以下のコマンドを入力してください。

$ sudo ifconfig
en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    ether xx:xx:xx:xx:xx:xx
    inet6 xxxx::xxxx:xxx:xxxx:xxx8%en0 prefixlen 64 scopeid 0x4
    inet 10.200.2.130 netmask 0xffff0000 broadcast 10.200.255.255
    nd6 options=1<PERFORMNUD>
    media: autoselect
    status: active
ppp0: flags=8051<UP,POINTOPOINT,RUNNING,MULTICAST> mtu 1500
    inet 10.100.0.5 --> 10.255.254.0 netmask 0xffff0000

en0 はイーサネットのネットワークです。つまりLANですね。 ppp0 はPPTPのVPNです。
そして、en0のMTUは 1500 で、 ppp0のMTUは1500 である事が確認できます。
問題はこのppp01500という値です。

MTU値を変更

/etc/ppp/ip-up というパスのファイルを作成して、以下の内容を記述します。

#!/bin/sh

/sbin/ifconfig ppp0 mtu 1300

また、ファイルのパーミッションを755 に変更してください。

$ sudo chmod 755 /etc/ppp/ip-up

ip-upというファイルは、VPNに接続するたびに実行されるスクリプトです。
これで、接続時にMTU値が1300に調整されるようになりました。
試しに再接続してみてください。
この値をいろいろ変えてみて、安定する値を探ってください。

僕の場合はMac OSX Yosemiteで、 500 で安定しています。

PPTPマルチパススルー機能のついたルータを買う

複数人がVPNに接続する、または複数台が同時にVPNに接続する環境の人に有効です。

ルータは、VPNのパケットをアドレス変換(NAT)しないで通す機能があり、これをPPTPパススルーと言います。
家庭用のルータなどでは、この機能は1台までしか使えず、同時に複数の接続があると正しく対処できません。
そのため、同時接続して通信すると混線が発生して頻繁な切断を引き起こします。
この問題は、PPTPマルチパススルーに対応したルータを使う事で解決します。

自動再接続スクリプトを書く

上記2つのアプローチでも尚安定しない場合は、自動で再接続するApple Scriptを書くのが有効です。
簡単ですので、試してみてください。Yosemiteでも動作確認済みです。

ネットワーク名を確認する

VPN-network-preferences

この画面では、VPN接続名は「VPN (PPTP)」である事がわかります。
これを覚えておきます。

AppleScriptを書く

以下のようなアイコンの「アプリケーション」→「ユーティリティ」→「スクリプトエディタ」を開きます。

Script Editor

新規作成して、以下のスクリプトを貼り付けてください。

on idle
  tell application "System Events"
    tell current location of network preferences
      set myConnection to the service "VPN (PPTP)"
        if myConnection is not null then
          if current configuration of myConnection is not connected then
            connect myConnection
          end if
        end if
      end tell
    return 120
  end tell
end idle

「VPN (PPTP)」のところを、あなたのVPN接続名に置き換えてください。
以下のような画面になります。

VPN-Script Editor

できたら、以下のような設定で保存します。

Script Editor Export

スクリプトを起動する

保存したスクリプトを、VPNが切断された状態で起動してみてください。
自動でVPNが接続されましたか?やりましたね!
ためしに手動で切断してみてください。しばらくすると、再接続を試みるはずです。
接続状態をチェックする間隔は2分間ですが、変更できます。
スクリプト中のreturn 120という箇所を、任意の秒数に変更してください。

参考

jshintで”Redefinition of Promise (W079)”が出た時の対処方法

jshint v2.5.x 以降で、以下のようなコードを書くと、jshintさんに叱られます:

var Promise = require('bluebird');

そんな時は、.jshintrcファイルに以下の項目を追記しましょう:

{
  "predef": [ "-Promise" ]
}

参考

Evernoteでノートブックをグループ化する方法

Evernote

出来ないと思い込んでいたらすごく簡単に出来たのでご紹介。
操作としては、 iPhoneのホーム画面でアプリをグループ化するのと全く同じ方法 で出来ます!
Evernote公式のビデオがわかりやすかったので、ご覧ください。

キャプチャから方法をかいつまんで説明します。

Evernote_stack_1

グループ化したいノートブックをクリックしたままマウスを動かして、別のノートブック上で指を離します(ドラッグ&ドロップ)。
すると、2つのノートブックがグループ化されて一つになります。

Evernote_stack_2

グループを2回クリックすると、中身が展開されて、グループ化した2つのノートブックが表示されます。簡単ですね!

Evernote_stack_3

Evernoteでは、このグループのことを スタック(Stack) と呼んでいます。
Googleなどで検索する際にはStackと打つと情報が見つけやすいと思います。

MongoDB v2.8.0-rc0 がリリースされた

昨日の深夜に、MongoDB v2.8.0-rc0(Release Candidate) がunstableでリリースされました。Stable releaseまでいよいよ秒読み開始です。
個人的にアツイのは「Improved Concurrency」なので少し説明します。

ドキュメントレベルのロック

待ちに待ったdocument-level lockingです。
MongoDBは書き込みが弱い事が自分の中で有名(笑)ですが、それが大幅に改善されます。

MongoDB 2.6(stable)では、データベースレベルの書き込みロックです。
つまり、あるデータベースのコレクションに対して書き込みを行っている間は、同じデータベースのどのコレクションに対する読み書きもできないということです。
これがドキュメントレベルになることによって、同一のドキュメント以外なら読み書きできるようになる、という事です。

下図は、カンファレンスでのデモンストレーションの様子のキャプチャです。
同時に複数のクライアントが書き込みオペレーションを実行しています。
赤いグラフで縦軸が書き込み数です。左側の小さい山がdb-level lockingで、右の大きな山がdoc-level lockingです。
書き込みパフォーマンスが大幅に改善している事がわかります。

mongodb2.8-document-level-locking

昔は書き込みロックはグローバルだったのを考えると、やっとまともに使えるようになったなぁという感じです。

そのほかのアップデート

プラッガブルなストレージエンジン

MongoDBのストレージエンジンはあんまり性能が良くない事で評判なんですが、プラッガブルになった事でMySQLみたいに付け替え可能になりました。
これで有志による性能がより高いストレージエンジンの開発が期待されます。

圧縮

On-dick compressionによってI/O効率が30〜80%よくなるそうです。

参考

さらに詳しい情報は以下をチェックしてください。