jQueryプロジェクトから学ぶユニットテスト

この記事は jQuery Advent Calendar 2014 の13日目の記事です。

このエントリーでは jQuery プロジェクトでどのようにユニットテストが書かれているかをさらっとご紹介します。
jQueryプロジェクトでは QUnit というテスティングフレームワークが使用されています。
jQueryのプロジェクトチームによって開発され、jQuery や jQuery-ui などのライブラリで使用されているため、信頼性のあるフレームワークです。
そしてこの本家 jQueryチーム の開発プロセスからユニットテスト手法を学びます。

jQueryプロジェクトのユニットテストから学ぶ

規模が小さく期待する振る舞いが掴みやすい方がわかりやすいと思うので、
ここではjQuery UIのaccordionのユニットテストを見てみます。

HTML

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>jQuery UI Accordion Test Suite</title>

    <script src="../../jquery.js"></script>
    <link rel="stylesheet" href="../../../external/qunit/qunit.css">
    <script src="../../../external/qunit/qunit.js"></script>
    <script src="../../../external/jquery-simulate/jquery.simulate.js"></script>
    <script src="../testsuite.js"></script>
    <script>
    TestHelpers.loadResources({
        css: [ "core", "accordion" ],
        js: [
            "ui/core.js",
            "ui/widget.js",
            "ui/accordion.js"
        ]
    });
    </script>

    <script src="accordion_test_helpers.js"></script>
    <script src="accordion_common.js"></script>
    <script src="accordion_core.js"></script>
    <script src="accordion_events.js"></script>
    <script src="accordion_methods.js"></script>
    <script src="accordion_options.js"></script>

    <script src="../swarminject.js"></script>
    <style>
    #list, #list1 *, #navigation, #navigation * {
        margin: 0;
        padding: 0;
        font-size: 12px;
        line-height: 15px;
    }
    </style>
</head>
<body>
<div id="qunit"></div>
<div id="qunit-fixture">

<div id="list1" class="foo">
    <h3 class="bar">There is one obvious advantage:</h3>
    <div class="foo">
        <p>
            You've seen it coming!
            <br>
            Buy now and get nothing for free!
            <br>
            Well, at least no free beer. Perhaps a bear, if you can afford it.
        </p>
    </div>
    <h3 class="bar">Now that you've got...</h3>
    <div class="foo">
        <p>
            your bear, you have to admit it!
            <br>
            No, we aren't <a href="#">selling bears</a>.
        </p>
        <p>
            We could talk about renting one.
        </p>
    </div>
    <h3 class="bar">Rent one bear, ...</h3>
    <div class="foo">
        <p>
            get two for three beer.
        </p>
        <p>
            And now, for something completely different.
        </p>
    </div>
</div>

<div id="navigationWrapper">
    <ul id="navigation">
        <li>
            <h2><a href="?p=1.1.1">Guitar</a></h2>
            <ul>
                <li><a href="?p=1.1.1.1">Electric</a></li>
                <li><a href="?p=1.1.1.2">Acoustic</a></li>
                <li><a href="?p=1.1.1.3">Amps</a></li>
                <li><a href="?p=1.1.1.4">Effects</a></li>
                <li><a href="?p=1.1.1.5">Accessories</a></li>
            </ul>
        </li>
        <li>
            <h2><a href="?p=1.1.2"><span>Bass</span></a></h2>
            <ul>
                <li><a href="?p=1.1.2.1">Electric</a></li>
                <li><a href="?p=1.1.2.2">Acoustic</a></li>
                <li><a href="?p=1.1.2.3">Amps</a></li>
                <li><a href="?p=1.1.2.4">Effects</a></li>
                <li><a href="?p=1.1.2.5">Accessories</a></li>
                <li><a href="?p=1.1.2.5">Accessories</a></li>
                <li><a href="?p=1.1.2.5">Accessories</a></li>
            </ul>
        </li>
        <li>
            <h2><a href="?p=1.1.3">Drums</a></h2>
            <ul>
                <li><a href="?p=1.1.3.2">Acoustic</a></li>
                <li><a href="?p=1.1.3.3">Electronic</a></li>
                <li><a href="?p=1.1.3.6">Accessories</a></li>
            </ul>
        </li>
    </ul>
</div>

<dl id="accordion-dl">
    <dt>
        Accordion Header 1
    </dt>
    <dd>
        Accordion Content 1
    </dd>
    <dt>
        Accordion Header 2
    </dt>
    <dd>
        Accordion Content 2
    </dd>
    <dt>
        Accordion Header 3
    </dt>
    <dd>
        Accordion Content 3
    </dd>
</dl>

</div>
</body>
</html>

マークアップのテスト

  • accordion_core.js
module( "accordion: core", setupTeardown() );
// セレクタのハッシュリストを$.eachで回す
$.each( { div: "#list1", ul: "#navigation", dl: "#accordion-dl" }, function( type, selector ) {
    test( "markup structure: " + type, function() {
        expect( 4 ); // アサーションの数を4つに指定
        var element = $( selector ).accordion();
        // DOM要素にui-accordionというクラスがあるか
        ok( element.hasClass( "ui-accordion" ), "main element is .ui-accordion" );
        // .ui-accordion-headerが3つ存在しているか
        equal( element.find( ".ui-accordion-header" ).length, 3,
            ".ui-accordion-header elements exist, correct number" );
        // .ui-accordion-contentが3つ存在しているか
        equal( element.find( ".ui-accordion-content" ).length, 3,
            ".ui-accordion-content elements exist, correct number" );
        // .ui-accordion-headerの次の要素が.ui-accordion-contentかどうか
        deepEqual( element.find( ".ui-accordion-header" ).next().get(),
            element.find( ".ui-accordion-content" ).get(),
            "content panels come immediately after headers" );
    });
});

このテストはjQuery-ui accordionを適用した際に正しく各要素にクラスが付与されているか、要素が存在しているかのテストになります。
マークアップの正確性をテストするためのもので、要素が存在するかどうかというのは
JSでUIコンポーネントを生成したりする場合に応用が効くのではないでしょうか。

イベントのテスト

  • accordion_events.js
module( "accordion: events", setupTeardown() );

test( "create", function() {
    expect( 10 ); // アサーションの数を10に指定

    var element = $( "#list1" ),
        headers = element.children( "h3" ),
        contents = headers.next();

    // accordionを適用
    element.accordion({
        create: function( event, ui ) {
            // ヘッダーが存在するかどうか
            equal( ui.header.length, 1, "header length" );
            // 引数で渡ってきた要素の参照チェック
            strictEqual( ui.header[ 0 ], headers[ 0 ], "header" );
            // コンテンツ部分が存在するかどうか
            equal( ui.panel.length, 1, "panel length" );
            // 引数で渡ってきた要素の参照チェック
            strictEqual( ui.panel[ 0 ], contents[ 0 ], "panel" );
        }
    });
    // 意図的にaccordionの適用を削除
    element.accordion( "destroy" );

    // accordionを再適用
    element.accordion({
        active: 2, // 初期表示に2つ目を指定
        create: function( event, ui ) {
            // ヘッダーが存在するかどうか
            equal( ui.header.length, 1, "header length" );
            // 引数で渡ってきた要素の参照チェック(2個目か)
            strictEqual( ui.header[ 0 ], headers[ 2 ], "header" );
            // コンテンツ部分が存在するかどうか
            equal( ui.panel.length, 1, "panel length" );
            // 引数で渡ってきた要素の参照チェック(2個目か)
            strictEqual( ui.panel[ 0 ], contents[ 2 ], "panel" );
        }
    });
    // 意図的にaccordionの適用を削除
    element.accordion( "destroy" );

    // accordionを再適用
    element.accordion({
        active: false, // 初期表示はなし
        collapsible: true,
        create: function( event, ui ) {
            // uiオブジェクトに要素の参照がないことを確認
            equal( ui.header.length, 0, "header length" );
            equal( ui.panel.length, 0, "panel length" );
        }
    });
    // 意図的にaccordionの適用を削除
    element.accordion( "destroy" );
});

createが発火したタイミングで引数で渡ってくるuiオブジェクトの内容をテストしています。
意図的にイベントをtriggerしてあげれば、イベントオブジェクトの内容をチェックすることも可能なわけですね。

メソッドのテスト

  • accordion_methods.js
module( "accordion: methods", setupTeardown() );

// accordion('enable') / accordion('disable') をテスト
test( "enable/disable", function() {
    expect( 7 ); // アサーションの数を7つに指定
    var element = $( "#list1" ).accordion();
    state( element, 1, 0, 0 ); // accordionテスト独自関数
    element.accordion( "disable" ); // accordionを無効化

    // 無効化状態クラス(.ui-state-disabled)が追加されているか
    ok( element.hasClass( "ui-state-disabled" ), "element gets ui-state-disabled" );
    // aria-disabled属性値が設定されているか
    equal( element.attr( "aria-disabled" ), "true", "element gets aria-disabled" );
    // 無効化スタイルクラス(.ui-accordion-disabled)が追加されているか
    ok( element.hasClass( "ui-accordion-disabled" ), "element gets ui-accordion-disabled" );

    // ヘッダーのクリックイベントを発火
    element.find( ".ui-accordion-header" ).eq( 1 ).trigger( "click" );
    state( element, 1, 0, 0 );
    // オプションを設定
    element.accordion( "option", "active", 1 );
    state( element, 0, 1, 0 );
    // アコーディオンを有効化
    element.accordion( "enable" );
    // オプションを再設定
    element.accordion( "option", "active", 2 );
    state( element, 0, 0, 1 );
});

こちらもイベントのテストと似ていますが、メソッドが呼ばれた後のDOM要素の状態をテストしています。
明示的にtrigger()を実行することで、状態の変化・確認を行っています。

まとめ

今回は jQuery-ui のテストをものすごくサラッとご紹介しました。
主にDOMのテストですが、DOMのテストは広い範囲で応用できると思います。

本当はユニットテストとは何か?ってところから書きたかったのですが、
ボリュームがハンパなくなってしまったので、jQuery-ui のユニットテストに絞った内容にしました。

ユニットテストについては後日改めて書きたいと思います。

明日は 近藤直人さん です。

12/14追記

ユニットテストについての記事を書きました。併せて参考にしてください。

五十川 洋平(Yohei Isokawa)

五十川 洋平(Yohei Isokawa)

フロントエンドエンジニア/面白法人カヤックなどのWeb制作会社に勤務したのち、故郷の新潟に戻り独立。JSフレームワークAngularやFirebase、Google Cloud Platformを使ったWebアプリ開発が得意。 また、Udemyのプログラミング解説の講師、writer.appの自主開発や上越TechMeetupの主催などを行っています。

プロフィール

©Copyright 2022 Yohei Isokawa All Rights Reserved.