yo_waka's blog

418 I'm a teapot

Google Closure LibraryのユニットテストをPhantomJSで実行するやつ

を2年前くらいに作ったのだけど、久々にGoogle Closure Libraryを使いたくなって、そうなるとテストも書かないとなので引っ張りだして使おうと思ったのですが、なにせ2年前に作ったものなので、ソースコードはレガシー感が漂ってて、使ってるPhantomJSのAPIもdeprecatedになっていたりしたのでいろいろ直してアップデート(リポジトリ名も少し変更)。

waka/closure-library-phantomjs

単一のテストファイルのテストでもマルチテストランナーを使った複数テストファイルのテストでも同じ使い方です。
詳しい使い方やオプションは、READMEに書いた。

最近Mochaでテストを書くことが多いのですが、出力形式をいろいろ選べるの楽しくていいなと思ったので、インターフェース切ってリポーターをいくつか使えるようにした。

実際よく使うのは、見やすいSpec形式と、Jenkins等で扱うためのTAP形式と、テスト数が多い場合のDot形式かなと思ったので、その3つを使えるようにした。
これで、手元での開発に加えて、JenkinsでもTAP-Plugin使ってCIしやすくなった。

昔はQt入れなきゃいけなかったりして、PhantomJSのビルドが結構大変だった記憶があるのだけど、今のPhantomJSはバイナリが提供されていて、すぐに使えて便利。
Node.jsが入ってる環境なら、npmでも入れられる。
Node.jsが使える環境で、コマンドラインからもっと手軽に使いたい場合は、npmにもアップしてあるのでそっちからも使えます。
npm/closure-library-phantomjs
(npmからインストールしない場合はlib以下のJSをどこかに置いてPhantomJSから起動すれば使えます)

PhantomJSの変わった点といえば、CommonJSのmodule1.1をサポートするようになったのは大きい。
これによって起動スクリプト内で他のスクリプトを読み込めるので、起動スクリプトを分割しやすくなった。

あと、1.2から追加されたWebpageモジュールのinjectJSがかなり素晴らしい。
ClosureLibraryのテストランナーはブラウザ実行、つまりDOMに結果を書き出すのが前提で、そのままPhantomJSのconsoleにテストランナーのconsole.log出力を流しても欲しいログが取れない。

なので、テストの実行時間やエラーのスタックトレースを取得するために、goog.testing.TestRunnerやgoog.testing.MultiTestRunnerを拡張したりメソッドを差し替えたり、ということを以前はテストHTML内にスクリプトタグを追加してやっていたのだけど、これだとテストファイル1つ1つに差し込んでいかないといけないので効率が悪いし差し込むの忘れる。

そこでWebpage#onInitializedのハンドラでinjectJSを使うと、ページがロードされてからwindow.onloadが走る直前にスクリプトを差し込めて実行できる。これでテストランナーのscriptタグが読み込まれてオブジェクトを参照可能かつテストランナーが走る前、というタイミングでメソッドを拡張したり差し替えることができます。素晴らしい。

Google Closure LibraryでUI作ってユニットテストも書いて、、って開発しているとこなんてほとんど無いと思うけど、Google Closure Libraryのマルチテストランナーはかなり使い勝手がよいですし、非同期テストもサポートされていてアサーションも完備。

たしかQUnitは複数のHTMLテストファイルを実行できなかったと思うので、複数実行したい場合のテストライブラリとしてGoogle Closure Libraryを使うというのもアリなんじゃないかと思います。
よく飼い慣らされたClosure Library野郎であれば、当然テストのためだけにClosure Libraryを使う。

bowlというNode.jsのcluster管理モジュールを作った

cluster周りのコードを書いていて、ワーカー周りの死活監視やエラー処理などいつも同じようなこと書いてるのでモジュール化してみました。
bowl

GitHubにソースコードもあげてあります。
安心のテストコード付き!
waka/node-bowl

できること

  • コマンドラインスクリプトからの起動
  • ワーカーが死んだら新しいワーカーを立ち上げ
  • 停止シグナル(SIGINT, SIGQUIT)
  • SIGUSR2シグナルによるGraceful Restart(この辺にも対応
  • 指定したディレクトリ/ファイルに変更があったら再起動(Graceful Restart)
  • プラグインによるマスタープロセスの拡張
  • pidファイルの書き出し(monitなどの監視を考えて)

設定の外部ファイル化や機能のオン/オフなどもできるので、詳しい使い方はヘルプコマンドにまとめた。

開発メモ

ファイルの変更監視にはfs.watchFile、使えない環境ならfs.watchを利用します。
node-devみたいにmodule.requireを書き換えるやり方もありますが、デフォルトの挙動を差し替えるのは開発環境でしか使えないよなーと思って、ディレクトリとファイルを指定するやり方にしました。
ディレクトリは、子階層もちゃんと見るようにしています。
ただ、fs.watchFile/watchの仕様ぽいんだけど、ファイルの削除とリネームは変更イベントが起きない。ディレクトリで指定するとイベント発生するのに・・!要調査。

forkするワーカーの起動時にエラーが起きた場合は、domainのエラーハンドラでは対処出来ないので、起動後10秒以内に10回以上ワーカーのエラーが発生したら終了するようにしています。

また、Node.jsの対応バージョンが0.8.xとしているのは、ロガーとして使っているwinstonを0.9.xで動かしたところどうもファイルのtransportを使うと最初の1回しか書き込まれない現象が起きているから。
winstonは連続でログを書き込もうとしたときにキューに溜めておいて、drainイベントで書き込めるようになったら書き込みを再開するのだけど、drainイベントがなぜか発生しない。
0.9.xからはstream周りがかなり変わるみたいな話をどこかで見た気がしたので、ソースを追ってみようと思ったけどかなり複雑でちょっと後回しにしちゃった。
winstonはtransportを柔軟に設定可能なところと、ログローテートが出来る点が好きなライブラリなので、ちょっと時間ができたらちゃんと追ってみよう。

今回テスト用に初めてTravis CIを使ってみたけど、.travis.yml置いてプッシュするだけとか簡単すぎてやばい。
GitHub周りのエコシステムは仕組みといい使い勝手といい素晴らしいな。

Node.jsアプリで結果が非同期になる箇所はコールバックを渡せるようにした方がいい

既存のNodeアプリのテストを書いていて思ったことをメモ。
書いてるうちに別にNode.jsに限った話じゃなくて、JavaScript全般に言える話じゃんと思ったけどまいっか。

Node.jsで書くアプリは中の処理で非同期API使われてると、テストを書くのがとても難しいというかめんどくさい。

例えばfsモジュールで非同期でファイル書いてる処理があって、もし書き込みに失敗したらエラーイベントを投げて上のレイヤーで処理する、みたいなのがあるとする。

// HogeクラスはEventEmitterを継承している
Hoge.prototype.writeFile = function(file, data) {
  var self = this;
  fs.writeFile(file, data, function(err) {
    if (err) {
      self.emit('error.file', err.message);
    }
  });
};

アプリ側では"error.file"イベントを捕まえてよしなにやるからいいんだけど、テストを書こうとするととてもめんどくさい。
ファイルに書き込んだら適当な時間待ちつつ、エラーイベントが捕まったら失敗にする。

// mochaを使った例
it('ファイル書けるか', function(done) {
  var timer;
  var hoge = new Hoge();
  hoge.on('error.file', function(errMessage) {
    clearTimeout(timer);
    assert.ok(false, errMessage);
  });
  hoge.writeFile('./test.txt', 'test');
  timer = setTimeout(function() {
    done();
  }, 1000);
});

適当な時間待つのがとてもスマートじゃない感じ。。。
これだけだったらいいけど、他にもいろんなところで非同期APIを中で使ってるとテストコードがsetTimeoutの嵐に><

この問題は、アプリ側の方でもコールバックを取れるようにしてあるといい感じにできる。

Hoge.prototype.writeFile = function(file, data, opt_callback) {
  var self = this;
  fs.writeFile(file, data, function(err) {
    if (err) {
      self.emit('error.file', err.message);
    }
    opt_callback && opt_callback(err || null);
  });
};

// テスト
it('ファイル書けるか', function(done) {
  var hoge = new Hoge();
  hoge.writeFile('./test.txt', 'test', function(err) {
    if (err) {
      assert.ok(false, err.message);
    }
    done();
  });
});

タイマー処理の必要がなくなり、とてもスッキリと書けるようになりました。
普段からNodeアプリを作ってる人には当たり前の話かもしれませんが、要は中で非同期API使ってるメソッドは外側からコールバックを渡せるようにしておくとテスト書きやすいですという話。
アプリで使ってなくても、オプション引数としてJSDocに書いておけばよいですし。
新しくNode.jsアプリを作るときはコーディング規約にしたいなー

既存のアプリですべての該当箇所を変えるのはそれはそれでいろいろ大変ですが。。

Node.jsのcluster.disconnectの挙動とGracefulリスタート

clusterモジュールを使ってサーバアプリ管理ツールを作ってるのだけど、cluster.disconnectのコールバックが実行されてもワーカープロセスが死なないことがある。
Chromeでサーバにアクセスすると、cluster.disconnectしてもワーカーの'exit'イベントが発火されずにプロセスが残ってしまう。

しばらく放置しておくとワーカープロセスが死ぬんだけど、これはどういうことだと思ったらこういうことらしい。keep-aliveかー
タブを閉じてもダメ。どうもChromeのkeep-aliveのタイムアウト時間は5分みたいですね。
先の記事でいうところの、「すべてのコネクションが終了し、サーバが 'close' イベントを発したときに最終的に閉じます。」という実装をちゃんとしていないサーバの場合、5分経つまで接続が閉じない。

サーバ側の実装がよくないのが根本的な原因なんだけど、それはそれとして、そんなサーバでもcluster.disconnectを使ってGracefulな再起動をやりたい場合はどうしよう。

いろいろ考えてみたけど、ワーカーを個別に監視して一定時間後に死んでいなければ強制的にワーカーのdestroyを呼んでやるのがいいのかなーと思った。
clusterの'disconnect'イベントハンドラでタイマーでワーカーのdestroyを呼ぶ処理をしておいて、'exit'イベントハンドラでタイマーをクリアしてあげればいい。

ざっくりとこんな感じ。

var forks = 3;
var destroyTimers = {};

cluster.on('exit', function(worker, code, signal) {
  var timer = destroyTimers[worker.id];
  if (timer) {
    clearTimeout(timer);
    delete destroyTimers[worker.id];
  }
});

cluster.on('disconnect', function(worker) {
  destroyTimers[worker.id] = setTimeout(function() {
    worker.destroy();
  }, 5000);
});

// ワーカー再起動
function restart() {
  killWorkers();
  forkWorkers(function(err) {
    if (err) {
      return;
    } 
    console.log('Restarted workers');
  });
}

function forkWorkers(opt_callback) {
  var domain = Domain.create();

  domain.on('error', function(err) {
    var errMessage = 'Failed to start new workers.\n' + err.message;
    console.error(errMessage);
    opt_callback && opt_callback(new Error(errMessage));
  }); 

  domain.run(function() {
    for (var i = 0; i < forks; i++) {
      cluster.fork();
    }   
    opt_callback && opt_callback();
  });
}

function killWorkers(opt_callback) {
  var domain = Domain.create();

  domain.on('error', function(err) {
    var errMessage = 'Failed to disconnect workers.\n' + err.message;
    console.error(errMessage);

    // 強制的にワーカーをdestroyするメソッド
    killWorkersForce(opt_callback);
  });

  domain.run(function() {
    cluster.disconnect(function() {
      opt_callback && opt_callback();
    });
  });
}

これでrestartメソッドで古いワーカーが遅くとも5秒後にはすべて死ぬようになった。

コールバックもちゃんとやるなら、全部のワーカーが立ち上がったか/死んだかをチェックして実行しないとダメ ('listening'か'online'イベントハンドラが何回呼ばれたかチェックする感じ?)。

cluster.forkでエラーになった際にDomainsのエラーハンドラで何もしていないのは、この場合ワーカーのスタートアップ処理に致命的な不具合があると思われるから。
コンソール上で派手にエラーアピールしてあげる。

タイマーのインターバルは、サーバアプリの応答時間にもよるので設定で変えられるようにするのがよさげ。

あけまして

明日から仕事始まる、、、ウワーン・゚・(Pд`q。)・゚

もう6日ですが、あけましておめでとうございます。

今年はもうちょっと個人でもいろいろアウトプットできるようにしていきたいなー。

特にぜんぜん広まる気配のないGoogleClosureLibraryがもうちょっと使われるように布教していきたいと思います!

今年もよろしくお願いします。