読者です 読者をやめる 読者になる 読者になる

QUnitでTDD(テスト駆動開発)を実践してみる

HTML5 QUnit テスト駆動開発

QUnitやTDDの概要は分かったが、まだまだ、テストのためのテストをやっている段階。
どうやって実務に活かせばいいのかは分かっていない。

そこで、このページの内容を読んでみた。
実践TDD! テスト駆動開発入門 : アシアルブログ

非常に分かりやすくて、実際の工程をイメージ出来た。
が、同時に、これは実際に自分でやらないことには習得できないなと感じた。
いくら教材やサンプルを読んで理解しても、身につかない。
実際の開発のなかに取り入れて実践しないと、習得できそうにない。

まずは小さな規模の開発で、取り入れてみよう。
ということで、以前から興味のあったWeb Storageの勉強がてら、簡単なウェブページをつくることにした。
前回の記事Web Storageを勉強したのはそのため。

今回作るのは、次のようなページ。

  • 3*3の9つのマス目があり、各マス目にボタンが設置してある。
  • このボタンを押すと、そのマス目に画像が現れたり消えたりする。
  • どのマス目にどの画像が表示されているかはWeb Storageに記録されており、ブラウザを閉じたり、ページをリロードしたりしても、直近の状態が保存されている。
  • 任意のタイミングでストレージを削除することも出来る

これを、TDDで作ってみる。

TDDの実践に専念したいので、htmlとcssは先に完成させた。
これを、tdd-storage.htmlとする。大した量でもないしメンテナンスも不要なので、htmlファイル内にcssも記載した。
あとは、処理を実装するだけである。

f:id:numb_86:20160413152457p:plain:w400

開発開始 DOMはどうすればいい?

まずは開発していくにあたっての準備。
全部で4つのファイルを用意する。

  • tdd-storage.html
    • 先ほど用意したファイル。script.jsを読みこませる。
  • script.js
    • 処理を記述していくファイル。
  • test.js
    • script.jsの内容をテストするファイル。
  • test.html
    • テスト結果を表示するファイル。script.jsとtest.jsの両方を読みこませる。

まずはストレージについては考えず、表示についてだけ実装しようと思った。

が、気付いた。 DOMをQUnitでテストするにはどうすればいいんだ?

すごく難しい気がしてきた。
取り敢えず今回は、DOMに関するものは先に作ることにした。大した処理はしないので、すぐに完成。
気を取り直して、ストレージ機能をTDDをやってみる。

最初のテスト

まず、引数にkeyを与えて、そのkeyに対応するストレージがあればその値を、無ければundefinedを返す、checkStorage関数を作ってみる。

QUnit.test("checkStorageの挙動を確かめるテスト", function() {
    localStorage.removeItem('storage_0');
    QUnit.equal(checkStorage('storage_0'), undefined, "checkStorage");
});

まだcheckStorage関数を用意していないのだから、当然、通らない。

f:id:numb_86:20160413152516p:plain

早速作ってみる。

checkStorage = function(key){
    if(localStorage[key] == undefined){
        return undefined;
    }
    else{
        return localStorage[key];
    };
};

これなら、さっきのテストは通る。

f:id:numb_86:20160413152709p:plain

ただ、これだと確認が不十分なため、テストの処理を追加する。

QUnit.test("checkStorageの挙動を確かめるテスト", function() {
    localStorage.clear();
    localStorage.removeItem('storage_0');
    QUnit.equal(checkStorage('storage_0'), undefined, "checkStorage");
    localStorage.storage_0 = 'data';
    QUnit.equal(checkStorage('storage_0'), 'data', "checkStorage");
});

f:id:numb_86:20160413152723p:plain

これも通った。ということで、問題なく動いていると判断できる。
だがこの関数は、テストのために作ったようなもので、機能としてはほとんど意味が無い。

テストの結果、Web Storageの仕様に対する理解が深まる

実用的な関数を作るため、設計を考える。

  • tddStorageというkeyにデータを保存する。
  • そのデータは配列で、0〜8までの要素がある。
  • 各要素の値として、man, woman, car, empty のいずれかをセットする。

まず、ブラウザにtddStorageがあるかどうかを確認し、なければ、空の配列として作成する。
その関数を作ろう。

checkStorage = function(){
    if(localStorage.tddStorage == undefined){ localStorage.tddStorage = []; };
};
QUnit.test("checkStorageの挙動を確かめるテスト", function() {
    localStorage.clear();
    QUnit.equal(localStorage.tddStorage, undefined, "テスト1");
    checkStorage();
    QUnit.equal(localStorage.tddStorage instanceof Array, true, "テスト2");
});

f:id:numb_86:20160413152741p:plain

なぜか通らず。

テストの書き方が悪いのか、実装が間違っているのか判断がつかないが、調べていく。

調べてみたところ、文字列しか保存できず、配列は保存できないことが判明。

ローカルストレージは文字列しか保存できないのでオブジェクトを保存することができません。
その問題を解決するために、JSONというデータフォーマットに変換して文字列として保存する方法がよく使われています。
STEP1-6.ローカルストレージを使ってみる / チームラボ オンラインスキルアップ課題

なるほど。
どうやら、テストの記述は正しかったようだ。
ということでまずは、JSON形式でのストレージの保存に取り組むことに。

JSON形式によるストレージの保存と、その取り出し

配列のままでは保存できないということが分かったので、設計を整理してみる。

  1. 画面の状態をDOMで取得
  2. 取得したDOMに基いて、配列を作成
  3. 配列を、JSONフォーマットに変換
  4. ストレージに保存

という流れだと思う。
描画する場合(画面読み込み時)は逆に、

  1. ストレージから情報を取り出す。情報がなければ何もせず処理完了。
  2. 取り出したJSONを配列に変換
  3. 配列の内容に基いてDOM操作して描画

という流れ。

上記の記事にJSONへの変換について書かれていたので、学習。

続いて、配列を与えたらJSONに変換してストレージに保存する関数を実装することに。
それから、ストレージからJSONを取り出して、それを配列にして返す関数も作る。

まずは保存するsaveStorage関数を作る。

saveStorage = function(array){
    array = JSON.stringify(array);
    localStorage.tddStorage = array;    
};
QUnit.test("saveStorageの挙動を確かめるテスト", function() {
    localStorage.removeItem('tddStorage');
    QUnit.ok(localStorage.tddStorage == undefined, "テスト1");
    var array = ['man', 'woman', 'car', 'empty'];
    saveStorage(array);
    QUnit.ok(localStorage.tddStorage != undefined, "テスト2");
});

f:id:numb_86:20160413152802p:plain

通った。
次は、ストレージを取り出し、それを配列にして返す関数。
ストレージがなければundefinedを返す。

loadStorage = function(){
    var array = localStorage.tddStorage;
    if (array == undefined) { return array };
    array = JSON.parse(array);
    return array;
};
QUnit.test('loadStorage', function(){
    localStorage.removeItem('tddStorage');
    var result = loadStorage();
    QUnit.ok(result == undefined, "ストレージが消去されているか確認");
    var array = ['man', 'woman', 'car', 'empty'];
    saveStorage(array);
    result = loadStorage();
    QUnit.equal(result[1], 'woman', 'ストレージを配列として取り出せているか確認');
});

f:id:numb_86:20160413152818p:plain

これも通った。
今まで実は何度かテストを書き直していたが、今回は一発でいけた。
テストを書くのに慣れてきた。

さて、ではここからどうしようかと考えたが、もう終わりだと思う。必要な機能は実装し終えた。
後は、DOM操作と組み合わせるだけ。

ということで、完成。

サンプル

ボタンを押すと各マス目に画像が表示される。その状態でタブを閉じ、再度開くと、前回の状態が保存されている。

収穫や反省点

規模が非常に小さかったのでメリットを感じにくいが、やはり便利だと思う。
手動でテストしなくて済むのは素晴らしい。デグレを過剰に恐れなくて済む。

テストの設計を問われる、というのを実感した。
どういうテストにすべきか、を考えないといけないから、一手間増える。
さらに、実装のほうも、テストできるような形に落とし込まないといけない。入力と出力を、テストの意図に沿ったものにしないといけない。
何をテストすべきか考え、それに基いてテストを設計し、それをテストできるような形での実装を考えないといけない。

面倒といえば面倒だ。ただ、慣れの問題だとは思う。

それに、この思考法でやれば、処理と表示の分離(プログラムとデザインの分離)や、関数は単一機能で、という守るべき原則を順守しやすくなると思う。
だから、むしろ習慣にすべき思考法のような気がしている。

それに、関数の挙動を細かく確認する習慣をつけることで、意図せぬ挙動を見過ごすリスクも減ると思う。
意図通りに動いたと安心していても、その裏で望ましくない処理も行っていた、というのはありがち。
それを早期に発見できる効果が期待できると思う。

課題としては、DOMでのTDDはどうやればいいのか、よく分からなかった。
クライアントサイドJavaScriptでのTDDは、どういう風にすればいいのだろうか。TDDというか、そもそも自動テストをどうすればいいのかがよく分からない。
今後は、それを調べたい。

それから、もうちょっと規模の大きいもので試したいとも思う。