30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

QUnitでDOM操作やクリックイベントをテストする方法

前回の記事で、DOM操作をテストする方法が分かっていないことに気付いた。
現実的に考えて、クライアントサイドJavaScriptでコーディングしていく上で、DOM要素やそれに紐付いたイベントを避けて通ることは出来ない。
これらをテストできなければ、テストできる範囲は大きく狭まってしまう。
ということで、調べてみた。

QUnitでDOM要素を扱う方法

調べてみたら参考になる記事が見つかり、やり方はとても簡単だということが分かった。
#qunit-fixtureの中に、扱いたいDOMを記述すればよい。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>QUnit</title>
  <link rel="stylesheet" href="http://code.jquery.com/qunit/qunit-1.22.0.css">
</head>
<body>
  <div id="qunit"></div>
  <div id="qunit-fixture">
    <!-- ここにテストするDOM要素を記述していく -->
  </div>
  <script src="http://code.jquery.com/qunit/qunit-1.22.0.js"></script>

  <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.2/jquery.min.js"></script>

</body>
</html>

これだけ。

指定したDOM要素のテキストを返し、当該要素がなければその旨を伝えるテキストを返すgetBoxText関数を作成する。

var getBoxText = function(num){
    if ($('#box'+num)[0] == undefined) { return '該当するDOM要素がありません。' };
    return $('#box'+num).text();
};

そして、#qunit-fixtureを編集した上で、下記のテストを実行してみる。

  <div id="qunit-fixture">
    <div id="box1">box1要素のテキスト</div>
    <div id="box2">box2要素のテキスト</div>
    <div id="box3">box3要素のテキスト</div>
  </div>
QUnit.test("DOM取得のテスト", function() {
    QUnit.equal(getBoxText(1), 'box1要素のテキスト', 'DOM取得のテスト');
    QUnit.equal(getBoxText(9), '該当するDOM要素がありません。', 'DOM取得のテスト');
    QUnit.equal(getBoxText(2), 'box2要素のテキスト', 'DOM取得のテスト');
});

無事に通ったので、DOM要素を取得できていることが分かる。

#qunit-fixtureの中身は、テストごとにリセットされる

DOM要素を操作しても、テストが終了すると自動的に、#qunit-fixtureの中身は元の状態に戻る。
そのため、他のテストを意識せずにテストコードを書ける。

DOMを操作するため、指定したDOM要素を削除するdeleteBox関数を作成。

var deleteBox = function(num){
    $('#box'+num).remove();
};

テストは以下の通り。

QUnit.test('DOM削除のテスト', function() {
    QUnit.equal(getBoxText(1), 'box1要素のテキスト', 'DOM削除のテスト');
    deleteBox(1);
    QUnit.equal(getBoxText(1), '該当するDOM要素がありません。', 'DOM削除のテスト');
});

QUnit.test('DOMが復活しているかのテスト', function(){
    QUnit.equal(getBoxText(1), 'box1要素のテキスト', 'DOMが復活しているかのテスト');
});

「DOM削除のテスト」で#box1の要素を削除しているが、「DOMが復活しているかのテスト」を実施する時点で、#box1が復活していることが分かる。

イベントのテスト

次は、DOM要素に紐付けたイベントをテストする方法。
これも、難しいことはなかった。

まずイベントオブジェクト(今回はクリックイベント)を作り、それをトリガーメソッドの引数として渡せばいいようだ。

var event = $.Event('click');
$('#box1').trigger(event);   // これで、#box1要素がクリックされたのと同じ効果が得られる

自身のテキストを変更する関数。それを#box1のクリックイベントに設定する。

$('#box1').on('click', function(){
    $(this).text('クリックされました。');
});

そしてテストコードで、そのイベントを発生させてみる。

QUnit.test("クリックイベントのテスト", function() {
    QUnit.equal(getBoxText(1), 'box1要素のテキスト', 'クリックイベントのテスト');
    var event = $.Event('click');
    $('#box1').trigger(event);       // ここで、クリックのイベントハンドラが実行される
    QUnit.equal(getBoxText(1), 'クリックされました。', 'クリックイベントのテスト');
});

これで、イベントハンドラの挙動もテストできる。

#qunit-fixtureの中身を動的に追加する

これで、DOM要素もQUnitで問題なくテストできるようになった。
しかし、テストしたい内容が変わる度に、手動でその都度#qunit-fixtureの中身を書き換えるのは手間だ。
テストの側で、#qunit-fixtureを変化させたい。

それ自体は、#qunit-fixture要素に対してDOM操作をすればいいわけで、難しくない。

var appendBoxElem = function(id, text){
    $('#qunit-fixture').append('<div id="box' + id + '">' + text + '</div>');
};

テストのなかでDOM要素を追加し、そのテストケースが終わればリセットされていることが確認できる。

QUnit.test("DOM挿入のテスト", function() {
    QUnit.equal(getBoxText(9), '該当するDOM要素がありません。', 'DOM挿入のテスト');
    appendBoxElem(9, '挿入したdiv要素')
    QUnit.equal(getBoxText(9), '挿入したdiv要素', 'DOM挿入のテスト');
});

QUnit.test('DOMが戻っているかのテスト', function(){
    QUnit.equal(getBoxText(9), '該当するDOM要素がありません。', 'DOMが戻っているかのテスト');
});

しかしこれも結局、DOMの記述をhtmlではなくJavaScriptでやっているだけの話であり、手間がかかることに違いはない。
これでTDDをやるのは現実的ではないように思う。
プロダクトの内容をそのまま#qunit-fixtureに再現させる方法はないだろうか。

ヒアドキュメント

プロダクトのDOMをそのまま取得し、それを#qunit-fixtureに挿入できればいい。
それを実現するために、ヒアドキュメントというものを利用できそうだ。

ヒアドキュメントとは、複雑な文字列を扱うための仕組みで、複数行の文字列も扱える。
これを使えば、複数行に渡るhtmlの記述を変数に入れておき、任意のタイミングで挿入することが出来る。

JavaScriptにはヒアドキュメントの仕組みはサポートされていないが、それを再現する方法を見つけた。

SafariでもエラーにならないJavascriptのヒアドキュメントの書き方 - Qiita

var 変数名 = (function(){/*
// 任意の文字列
*/}).toString().match(/[^]*\/\*([^]*)\*\/\}$/)[1];

これを利用すれば、複雑なDOMを#qunit-fixtureに追加できるようになる。

プロダクトのDOMをQUnitでテストする

プロダクトのhtmlファイルに、次のような記述があったとする。

<div id="main">
    <div id="box33" style="height: 100px; background-color: orange;">a</div>
    <div id="box34" style="height: 30px; background-color: yellow;">b</div>
    <div id="box35" style="height: 100px; background-color: orange;">c<br>d</div>
</div>

この#mainの中身をテストしたいとする。その場合、テストしたい部分をヒアドキュメントとして変数に格納すればいい。

var data = (function(){/*
<div id="box33" style="height: 100px; background-color: orange;">a</div>
<div id="box34" style="height: 30px; background-color: yellow;">b</div>
<div id="box35" style="height: 100px; background-color: orange;">c<br>d</div>
*/}).toString().match(/[^]*\/\*([^]*)\*\/\}$/)[1];

そして、ヒアドキュメントとして保持しているDOMを、任意のタイミングで#qunit-fixtureに挿入すればいい。

var appendDom = function(string){
    $('#qunit-fixture').append(string);
};

QUnit.test('ヒアドキュメントの挿入', function(){
    QUnit.equal(getBoxText(33), '該当するDOM要素がありません。', 'ヒアドキュメントの挿入');
    appendDom(data);
    QUnit.equal(getBoxText(33), 'a', 'ヒアドキュメントの挿入');
});
QUnit.test('DOM構造が元に戻っていることを確認', function(){
    QUnit.equal(getBoxText(33), '該当するDOM要素がありません。', 'DOM構造が元に戻っていることを確認');
});

これで機能的には、QUnitでかなりの部分をテストできるようになったはず。
実際の開発で実践していく際には、またいろいろと課題が出てきそうではあるが。

参考

以下の記事のおかげで、すんなり理解できた。