あいつの日誌β

働きながら旅しています。

休日プログラマのテスト作法(JavaScript編)

JavaScriptのテスト作法を考えるきっかけができたのでえいやと考えました。
実務で使うというより日曜プログラマ向けの内容だと思います。たぶん。

それはある日突然起きてしまった

youtubeApiを利用して「KYOSUKE HIMURO GIG at TOKYO DOME」のコマーシャル動画情報を取得したいなあ。という事で次のようなコードを書きました。

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    <title>Youtube Api Sample</title>
    <script src="http://code.jquery.com/jquery-latest.js"></script>
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
</head>
<body>
<script>

var vid = 'XRzU_em6h80';

try {
    $.ajax({
        cache: false,
        async: false,
        data: {
            "alt":"json"
        }, 
        url:"http://gdata.youtube.com/feeds/api/videos/" + vid,
        success: function (json) {
            console.log(json); // string or object ?
        },
        error: function(msg) {
            console.log(msg);
        }
    });
} 
catch (e) { 
    console.log(e);
}

</script>
</body>
</html>

上記のコードを応用してプログラミングを楽しんでいたのですが後でバグに苦しみました。
そうです。上記のコードはバグなのです。私はなかなかそれに気づかずに苦しみました。

原因: ブラウザによって返り値が異なる。

ええ!?なんだって??と思わず(氷室京介風に)叫ばずにはいられないまさかの事態。

Chrome(16.0.912.77)はオブジェクトとしてデータを返すのですが、FireFox(7.0.1)は文字列を返してきました。ああ、なんてこったい。

修正を加える

修正をしましょう。

success: function (json) {
    var data = typeof json === 'string' ? JSON.parse(json) : json;
    console.log(data); // object
},

よーしよし、めでたしめでたし...いやいやいや、問題はまだ残っている。

同じ失敗を繰り返さない為に

普段慣れないjavascriptなので自分が書いたコードが間違えていると思い込んでいたのでなかなか出口を発見できませんでした。
そしてこのちょっとしたことずいぶんと時間をかけてしまいました。

そもそも何が原因なのか?一番の原因は異なるブラウザでも動作するようにチェックをしていない事です。だって面倒くさい(え

じゃあどうする?

「そうだ!!JavaScriptでもテストをきちんと書こう!!」

どうやら俺にはjsTestDriverが必要らしい

異なるブラウザでも自分の書いた関数が意図した通りに動くかどうかをチェックする方法はjsTestDriverを試すで紹介したのでこれを採用したいと思います。

qUnit形式でテストを記述する

jsTestDriverにはQUnitAdapterなるものがあるのでコレを使えばqunit形式のテストを実行する事できます。

まずjsTestDriver.confを次のように設定します。(私はconf/jsTestDriver.confに置いています)

server: http://localhost:4224

load: 
 - ../root/js/*.js
 - ../tests/qunit/equiv.js  
 - ../tests/qunit/QunitAdapter.js  
 - ../tests/js/*.js

timeout: 60

equiv.jsとQunitAdapter.jsは該当の場所に設定して下さい。

tests/js/myapp_test.jsに簡単なテストをqUnit形式で記述して正しく動作するかを確認します。

% vi tests/js/myapp_test.js
test("a basic test example", function() {   
    ok( true, "this test is fine" );  
    var value = "hello";  
    equals( "hello", value, "We expect value to be hello" );  
});  

jsTestDriverを実行します。qUnit形式のテストが動作することが確認できました。

% script/jstestdriver.sh
Firefox 7.0.1 Mac OS [PASSED] Default Module.test a basic test example
Chrome 16.0.912.77 Mac OS [PASSED] Default Module.test a basic test example
Safari 534.51.22 Mac OS [PASSED] Default Module.test a basic test example
Total 3 tests (Passed: 3; Fails: 0; Errors: 0) (5.00 ms)
  Safari 534.51.22 Mac OS: Run 1 tests (Passed: 1; Fails: 0; Errors 0) (5.00 ms)
  Chrome 16.0.912.77 Mac OS: Run 1 tests (Passed: 1; Fails: 0; Errors 0) (0.00 ms)
  Firefox 7.0.1 Mac OS: Run 1 tests (Passed: 1; Fails: 0; Errors 0) (1.00 ms)

では実際にテストを作成してみます。先ほどのmyapp_test.jsを次のように書き換えます。

module("myapp module");

test('getVideo', function () {
    var vid = 'XRzU_em6h80';
    var data = getVideo(vid);
    ok(data);
	equals(data.entry.title.$t, 'KYOSUKE HIMURO GIG at TOKYO DOME');
});

実装します。

% vi root/js/myapp.js
function getVideo(vid) {
    var videoData;
    try {
        $.ajax({
            cache: false,
            async: false,
            data: {
                "alt": "json"
            },  
            url: "http://gdata.youtube.com/feeds/api/videos/" + vid,
            success: function (json) {
                videoData = typeof json === 'string' ? JSON.parse(json) : json;
            },  
            error: function (msg) {
                console.log(msg);
            }   
        }); 
    }   
    catch (e) { 
        console.log(e);
    }   

    return videoData ? videoData : null; 
}

テストを実行します。上記のスクリプトjQueryを使っていますので先ほどのconf/jsTestDriver.confで指定したパスでjquery.jsをloadする必要があります。
例えば root/js/jquery.js として配置しておけばよいです。

% script/jstestdriver.sh 
Safari 534.51.22 Mac OS [PASSED] myapp module.test getVideo
Chrome 16.0.912.77 Mac OS [PASSED] myapp module.test getVideo
Firefox 7.0.1 Mac OS [PASSED] myapp module.test getVideo
Total 3 tests (Passed: 3; Fails: 0; Errors: 0) (557.00 ms)
  Safari 534.51.22 Mac OS: Run 1 tests (Passed: 1; Fails: 0; Errors 0) (557.00 ms)
  Chrome 16.0.912.77 Mac OS: Run 1 tests (Passed: 1; Fails: 0; Errors 0) (541.00 ms)
  Firefox 7.0.1 Mac OS: Run 1 tests (Passed: 1; Fails: 0; Errors 0) (432.00 ms)

これでそれぞのブラウザでテストする事ができます。もしこのテストがあればちょっとした挙動の違いもすぐに発見できるので早く帰ることができるでしょう。

phantomJS + qunit-tapを試します。

とはいえjsTestDriverは起動に時間がかかってしまいます。一通り実装が完了した段階でjsTestDriverを起動するのは良いのですが、都度起動していては時間がかかりすぎます。
ということで phantomJS を使う事にします。

ファイルを設置

例えば次のようなファイルを準備します。

% cat script/phantomjs_test.sh
#!/bin/sh
URL=file://$PWD/tests/qunit.html
phantomjs tests/run_qunit.js $URL
% cat tests/qunit.html
<!DOCTYPE html>
<html>
<head>
  <meta content="text/html; charset=UTF-8" http-equiv="content-type">
  <title>QUnit Test Suite</title>
  <!-- target js file -->
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script>
  <script type="text/javascript" src="../root/js/myapp.js"></script>
  <link rel="stylesheet" href="./qunit/qunit.css" type="text/css" media="screen">
  <script type="text/javascript" src="./qunit/qunit.js"></script>
  <script type="text/javascript" src="./qunit-tap/qunit-tap.js"></script>
  <script>
    qunitTap(QUnit, function() { console.log.apply(console, arguments); }, {noPlan: true});
  </script>
  <!-- for testing file -->
  <script type="text/javascript" src="../tests/js/myapp_test.js"></script>
</head>
<body>
  <h1 id="qunit-header">QUnit Test Suite</h1>
  <h2 id="qunit-banner"></h2>
  <div id="qunit-testrunner-toolbar"></div>
  <h2 id="qunit-userAgent"></h2>
  <ol id="qunit-tests"></ol>
  <div id="qunit-fixture">test markup</div>
</body>
</html>

tests/run_qunit.jsは id:t-wada さんが作成したqunit-tapのsampleコードをそのまま持ってきます。
またqunitやらqunit-tapやらも色々と配置する必要があります。これらは説明が大変になるのでサンプルをgithubに置いてあるので後で試してみて下さい。

git clone git@github.com:okamuuu/Practice-JsTest.git
sh script/phantomjs_test.sh
sh script/jstestdriver.sh

テストを実行するとtap形式で結果が表示されます。

% script/phantomjs_test.sh  
# module: myapp module
# test: getVideo
ok 1
ok 2
1..2

まとめ

テストを書いてから実装をするテスト駆動開発を行う場合は phantomjs を使ってテスト。

% script/phantomjs_test.sh 

一通り実装が終わったら各ブラウザで挙動が同じになるかを確認

% script/jstestdriver.sh 

実務でJavaScriptを書いてる人はおそらく継続テストの事を考えてまだまだ色々やっていそうですが、私はJavaScriptに関しては休日プログラマなのでごれで十分だと思う。どうだろう?

本日の一曲

Mario Vazquez - Gallery