Niigata.LL で JSでTDDをどう実践しているのか をトークしました

ポータルサイトの方を見てもらうとわかりますが、VBAとかPowerShellとかもあって面白かったし、飛び入りで来てトークしてくれた方もいて面白かったし、PHPを話す人が居なかったのはなにか陰謀があったんじゃねーのかとか妄想できるし、濃密で良かったですよ。


トークタイトル:「JS TDD(and test) with your language

Webアプリを作っていると得意(メイン)の言語とは別に JavaScript に触れざるを得ないプログラマは多いんじゃない?
という観点で、非JSer対象にJSのTDD周りの話を。

※ 余談ですが、Niigata.LL では 18名の方の参加がありましたが、主にJavaScriptを書いているというJSerは2名ほど。開催日現在で Node.js 使っている方は 1名という状況で、思った以上に JSer いない! kanazawa.js だとあんなに女の子いっぱいいたのに! ひどい! とか思ったもんです。

念頭に置いた事など

  • Node.js のインストールを前提としない
  • ブラウザ上で動くJSを対象とした
  • 快適に TDD サイクルを回すことを目的に
  • 単体(ユニット)テストを対象とした
  • コマンドライン上でのテストを可能にする
  • モジュール(ファイル)の更新に合わせて、自動的にテストが走るようにする

Step 1. ブラウザ上でテストする

use QUnit

フレームワークには QUnitを チョイスしました。Test::More に似たフレームワークなので、Perl経験者には扱いやすいので。(RSpecなんかに馴染んでるRubyの人とかは、Jasmine あたりがいいんじゃないのかと。あと Mocha あたりもいいんじゃないかと)

ディレクトリ構造
  • Dancer とかの軽量のWAFだと、スタティックなファイルを配信するディレクトリが public/*/ とか多いので、開発対象となるモジュールファイルは public/js/ 配下においてみた。
  • テストに使う QUnit ライブラリは t/qunit/ 配下に置いた。ユーザーからはリーチしない
  • テストを記述したファイルは t/ 配下に。(この例は t/test.js)
  • テスト用のHTML は t/ 直下に。(この例だと t/index.html)
MyProject/
  |-- public/
  |   `-- js/
  |       `-- mod.js // テスト対象となるJSファイル
  |-- run-phantom.js // PhantomJS で テスト用HTMLを走らせるヘルパー
  `-- t/
      |-- index.html // テスト用HTML
      |-- qunit/
      |   |-- qunit-1.10.0.css
      |   `-- qunit-1.10.0.js
      `-- test.js    // テストを記述したファイル
QUnitライブラリのダウンロード
$ curl -O http://code.jquery.com/qunit/qunit-1.10.0.js
$ curl -O http://code.jquery.com/qunit/qunit-1.10.0.css

※ 2013.3.23 現在で qunit の最新版は 1.11.0 です。が、後ほどQUnit-TAPを使用する場合、1.11.0 だとうまくない場合があるので、1.10.0 を利用します。

テスト用HTMLの中身(t/index.html)

<!doctype html>
<head>
<meta charset="utf-8" />

<!-- テストフレームワークqunit.js & qunit.css -->
<link rel="stylesheet" href="./qunit/qunit-1.10.0.css" />
<script src="./qunit/qunit-1.10.0.js"></script>

<!-- テストを受けるモジュール-->
<script src="../public/js/mod.js"></script>
<!-- テスト本体 -->
<script src="./test.js"></script>

<title>TDD with QUnit</title><!-- title は記述することが強く奨励される -->
</head>

<body>
  <div id="qunit"></div><!-- テストの結果を表示する -->
  <div id="qunit-fixture"><!-- DOMテストする場合のフィールド、ここに書いたDOMはテストの度にinitされるので楽が出来る -->
  </div>
</body>

テスト対象のモジュールJS(public/js/mod.js)

function NyanError (message) {
    this.message = message;
    this.name    = 'NyanError';
}

function WannError (message) {
    this.message = message;
    this.name    = 'WannError';
}

(function () {
    NyanError.prototype = new Error;
    WannError.prototype = new Error;
})();

テストJS(t/test.js)
詳しいAPIは 「QUnit API Documentation」を参照してください

$(document).ready(ALL_TEST);

function ALL_TEST () {
    test('Assert', function () {
        var hoge = {hoge: 'HOGE'};

        deepEqual(hoge, {hoge: 'HOGE'}, JSON.stringify(hoge));

        equal(1, 1,   '1 == 1');
        equal(1, "1", '1 == "1"');

        strictEqual(hoge, hoge, JSON.stringify(hoge));
        strictEqual(1, 1, '1 === 1');
        //this error
        // strictEqual(1, "1",'1 === "1"');

        notDeepEqual(hoge, {hoge: /hoge/}
          , JSON.stringify({hoge: /hoge/.toString()}));

        notEqual(1, 2, '1 != 2');

        notStrictEqual(1, "1", '1 !== "1"');

        ok(true, 'true');

        // Error Test
        // 以下、public/js/mod.js の関数を使う
        throws(
            function () { throw new NyanError('にゃーーーーーん!!!'); }
          , NyanError // <- instanceof NyanError
          , 'raized error is an instance of "NyanError"'
        );

        throws(
            function () { throw new WannError('ワン!!!!!!!'); }
          , /ワン/ // <- 正規表現
          , 'raized error is an instance of "WannError"'
        );
    });

    test('AsyncTest - 非同期テスト', function () {
        stop();

        setTimeout(function () {
            ok(true);
            start();
        }, 500);
    });
}

テストの実行

$ open t/index.html

Step 2. CLI上でテストする

use PhantomJS
  • PhantomJS は 画面描画しないブラウザ(ヘッドレスブラウザ)
  • QtWebKitベースで、HTML5とかCSS3とか解析できる
  • でも画面キャプチャはできる

install PhantomJS

適当なディレクトリに http://phantomjs.org/download.html からバイナリをダウンロード & 展開

$ bin/phantomjs --version してバージョン確認出来ればOK

setup PhantomJS

# パスの確認
$ echo $PATH | perl -wpl -e 's/:/$&\n/'
# bin/phantomjs のシンボリックリンクをパスの通っているディレクトリに張る
$ sudo ln -s /Users/ishiduca/phantomjs/bin/phantomjs /usr/local/bin/phantomjs
# パスの確認
$ phantomjs --version

QUnit を PhantomJS から使うための runner.js を用意する

curl -O https://raw.github.com/jquery/qunit/master/addons/phantomjs/runner.js
mv runner.js run-phantom.js

PhantomJSでのテスト

$ phantomjs run-phantom.js t/index.html

# 結果
Took 604ms to run 12 tests. 12 passed, 0 failed.
# 12テスト中 成功: 12 失敗: 0

失敗すると

Test failed: undefined: Assert
    Failed assertion: raized error is an instance of "WannError", expected: null, but was: WannError: ワン!!!!!!!
    at file:///Users/ishiduca/MyProject/t/qunit/qunit-1.10.0.js:532
    at file:///Users/ishiduca/MyProject/t/test.js:37
    at file:///Users/ishiduca/MyProject/t/qunit/qunit-1.10.0.js:136
    at file:///Users/ishiduca/MyProject/t/qunit/qunit-1.10.0.js:279
    at process (file:///Users/ishiduca/MyProject/t/qunit/qunit-1.10.0.js:1277)
    at file:///Users/ishiduca/MyProject/t/qunit/qunit-1.10.0.js:383
Took 543ms to run 12 tests. 11 passed, 1 failed.

な感じで出る

Step 3. CLI環境でテストの詳細を出力する

use QUnit-TAP
  • コマンドライン向けに各々のテストの結果を出力するプラグイン
  • QUnit単体では標準出力する機能がないので、その部分を補完する
  • TAP形式で出力する
  • 作者が日本の技術者なので日本語で質問できる!

install QUnit-TAP

$ curl -O https://raw.github.com/twada/qunit-tap/master/lib/qunit-tap.js

出力先を指定するコードを t/index.html へ追加記入(<script src="./qunit/qunit-1.10.0.js"></script>の後に)

<script src="./qunit/qunit-tap.js"></script>
<script>
qunitTap(
    QUnit
  , function () { console.log.apply(console, arguments) }
);
// QUnit.config.autostart = false; // AMD形式で非同期でモジュールを読み込むときは必要
</script>

実行

$ phantomjs run-phantom.js t/index.html
# test: Assert
ok 1 - {"hoge":"HOGE"}
ok 2 - 1 == 1
ok 3 - 1 == "1"
ok 4 - {"hoge":"HOGE"}
ok 5 - 1 === 1
ok 6 - {"hoge":"/hoge/"}
ok 7 - 1 != 2
ok 8 - 1 !== "1"
ok 9 - true
ok 10 - raised error is an instance of TypeError
ok 11 - raised error message contains "type_error"
# test: AsyncTest - 非同期テスト
ok 12
# module: form data validator test
# test: ok pattern
ok 13 - form data validate test passed
# test: error pattern
ok 14 - account not pass test
1..14
Took 599ms to run 14 tests. 14 passed, 0 failed.

TAP形式で出力できるので TAPを対象にしたツールを利用できるようになるので、以下応用編!

Step 4. use prove

  • Perlプロダクトのコマンドラインツール
  • 基本的には t/以下のディレクトリのテストを実行する
  • --exeオプションを付けることで perl 以外のテストも実行できる
  • 結果をカラフルに(グリーン・レッド)
  • ex prove --exe node --ext .js -v

prove に食わせる test.sh を用意する

#!/bin/sh
phantomjs run-phantomjs $PWD/t/index.html

実行

$ prove -v --timer test.sh
[22:06:54] test.sh .. 
# test: Assert
ok 1 - {"hoge":"HOGE"}
ok 2 - 1 == 1
ok 3 - 1 == "1"
ok 4 - {"hoge":"HOGE"}
ok 5 - 1 === 1
ok 6 - {"hoge":"/hoge/"}
ok 7 - 1 != 2
ok 8 - 1 !== "1"
ok 9 - true
ok 10 - raised error is an instance of TypeError
ok 11 - raised error message contains "type_error"
# test: AsyncTest - 非同期テスト
ok 12
# module: form data validator test
# test: ok pattern
ok 13 - form data validate test passed
# test: error pattern
ok 14 - account not pass test
1..14
Took 572ms to run 14 tests. 14 passed, 0 failed.
ok     1961 ms
[22:06:56]
All tests successful.
Files=1, Tests=14,  2 wallclock secs ( 0.03 usr  0.01 sys +  0.53 cusr  0.14 csys =  0.71 CPU)
Result: PASS

実際は All tests successful のところがグリーンで表示されるのが気持ちいいです。

TDDによる開発の場合、素早くそのサイクルを回すことが肝要。なので、コード修正に合わせて(ファイルの更新に合わせて)テストを自動実行できる方が楽なので、そのためのツールを用意します。

Step 5. use pfswatch

  • Perlプロダクトのコマンドラインツール
  • 監視したいディレクトリやファイルの更新に合わせて任意のコマンドを実行する

監視の開始

$ pfswatch public/js/ -e prove -v --timer test.sh

public/js/以下のファイルが更新される度に、test.sh が走ってテスト結果を表示してくれます。

おまけ。

テストの結果を音声で通知させます。
僕の開発環境はMacBook13incなわけですが、スクリーンを2分割して片方にテストコードを表示させて、もう片方でモジュールのコードを書いているんですが、自動実行しているテストは裏で走らせてるので、結果が成功したのか失敗したのか通知してくれる仕組みがないと、ファイルの更新ごとにテスト結果を見に行かないといけない。これはよくない! ということで、今回は結果通知を音声に頼ってみました。

Step 6. use with-sound

  • Perlプロダクトのコマンドラインツール
  • コマンドが成功するか失敗するかによって、その結果に対応した音声が流れるアプリケーション

実行

$ with-sound phantomjs run-phantom.js t/index.html
# とか
$ pfswatch public/js/ -e with-sound prove -v --timer test.sh

まとめっぽいこと言う

慣れ親しんだ言語文化の資産を横流しすると便利

今回のトークでは テストフレームワークでは QUnit を使いました。
上記にもありますが Perlのテストフレームワーク Test::More に似たフレームワークだったからですし、Perlプロダクトの prove pfswatch with-sound というコマンドラインツールを使ってTDDを捗らせる工夫をしました。
けど別に Perl がいいよっていう話ではなく、自分の得意な言語文化を下敷きにするのもいいんじゃないのかなーと。そんな気分を話してみました。


最後に、Perl いいですよ