テスト駆動開発

モジュールレベル

Closure library におけるモジュール(ひとつの名前空間)単位のテスト駆動開発のワークフローです。

このモジュールの規模は goog.arraygoog.Uri 程度の規模を想定しています。

モジュールのスクリプトと一緒にテスト用ファイルをフォルダ(ディレクトリ)にいれておくと管理が楽になります。

実際、 Closure Library はそのようなフォルダ階層になっています(下は goog.array と goog.Uri のフォルダ構成例)。

JSUnit のテストコードを書く

モジュールごとに以下のような html を作成します。

Closure Library の慣例では テストするモジュール名_test.html となっています。

hoge_test.html

<!DOCTYPE HTML> <html lang="ja"> <head> <meta charset="UTF-8"> <title>Hogehoge testting</title> <script src="closure-library/closure/goog/base.js" type="text/javascript"></script> <script type="text/javascript"> goog.require('goog.testing.jsunit'); goog.require('hogehoge.Hoge'); </script> </head> <body> <script type="text/javascript"> function testGetHoge() { var hoge = new hogehoge.Hoge(); assertEquals('Hogeのget', 'Hoge!', hoge.get()); } function testSetHoge() { var hoge = new hogehoge.Hoge(); hoge.set('HOGE'); assertEquals('Hogeのset', 'HOGE', hoge.msg_); } </script> </body> </html>

test から始まる名前を持ち、グローバルに配置された関数一つが一つのテストケースと対応します。

testGetHoge は hogehoge.Hoge のインスタンスのメソッド get が 'Hoge!' を返さないときに失敗するテストケースです。

testSetHoge は hogehoge.Hoge のインスタンスのメソッド set によってセットされるプロパティ msg_ が 'HOGE' になっていないときに失敗するテストケースです。

モジュールコードを書く

hoge.js

goog.provide('hogehoge.Hoge'); hogehoge.Hoge = function() { this.msg_ = null; // <- 'Hoge!' がセットされていないといけない! }; hogehoge.Hoge.prototype.get = function() { return this.msg_; }; hogehoge.Hoge.prototype.set = function(msg) { this.msg_ = msg; };

テストを通過できるようにコードを書きます。

上のコードには this.msg_ の初期値のミスが含まれています。

ドキュメントを書く

Closure Library のすべてのコードにはドキュメントが付属しています。

下のコードを見てください。このコードは goog.array の一部分を翻訳したものです。

/** * 配列が空かどうかを判定する。 * @param {goog.array.ArrayLike} arr 判定したい配列。 * @return {boolean} 空であれば true 。 */ goog.array.isEmpty = function(arr) { return arr.length == 0; };

/** ... */ で囲まれた部分はコメントです。なにやら一定の規則に従って記述されているように見えますね。

実は、このコメンをつかって自動的に API ドキュメントを生成することができます。

このコメントはアノテーション(注釈)と呼ばれていて、次の規則で記述されています。

  • オブジェクトの説明

  • メソッドの引数の説明

      • @param {引数の型} 引数の名前 引数の説明

  • メソッドの戻り値の説明

      • @return {戻り値の型}

  • 説明対象のオブジェクト

このようにプログラムと一体化しているドキュメントには2つのメリットがあります。

  1. コーディングしながらドキュメントを書くことができる

  2. ドキュメントの保守性が向上する

1はともかく、2は少し不思議に思えたでしょうか。

ここではアノテーションだけでなくソースコードもドキュメントに利用している、というところが重要です。

たとえば、アプリケーションを開発しているときに名前空間の重複が発覚したとしましょう。

こういう場合は、名前空間を別の名前にして重複を回避することになります。

したがって、この場合は名前空間のメンバ名をすべて書き換える必要がありますね。

ドキュメントを Excel や Google Doc で管理している場合には、ソースコードとドキュメントの双方を書き換えなければなりません。

しかし、このソースコード埋め込み型のドキュメントはソースコードの内容からオブジェクトの名前を取得しているため、ソースコードの書き換えだけで済むのです。

ここで記述したアノテーションは JsDoc などのドキュメント生成ツールによって html へと出力することができます。

とくに JsDoc3 が Closure Library と相性が良いように思います。こちらについては姉妹 wiki のこちらで解説しています。

Closure Library のアノテーションは JSDoc アノテーションに似ていますが、部分的に違いがあります。

そこで、この wiki ではこれらを区別して Closure Library のアノテーションを Closure アノテーションと呼んでいます。

さて、モジュールのソースコードに Closure アノテーションを追加してみましょう。

// このモジュールはパブリックライセンス! /** * @fileoverview ほげほげなモジュール。 * @author あなたのメールアドレス (あなたの名前) */ goog.provide('hogehoge.Hoge'); /** * ほげほげなクラス。 * @constructor */ hogehoge.Hoge = function() { this.msg_ = null; // <- 'Hoge!' がセットされていないといけない! }; /** * メッセージを取得する。 * @return {string} メッセージ。 */ hogehoge.Hoge.prototype.get = function() { return this.msg_; }; /** * メッセージを設定する。 * @param {string} msg 設定したいメッセージ。 */ hogehoge.Hoge.prototype.set = function(msg) { this.msg_ = msg; };

Closure アノテーションの書き方については Closure アノテーションの記述方法 を参照してください。

JSUnit によりテストする

モジュールコードを書き終わったら、 hoge.js のテストファイル hoge_test.html を開きます。

すると以下のようなメッセージが表示されます。

Hogehoge testting [FAILED]/hogehoge/hoge_test.html 2 of 2 tests run in 4ms. 1 passed, 1 failed. 2 ms/test. 8 files loaded. . 18:25:59.647 Start 18:25:59.650 testGetHoge : FAILED (run individually) 18:25:59.650 ERROR in testGetHoge Hogeのget失敗 Expected (String) but was > (unknown) > Object.get at /hogehoge/google-closure-library/closure/goog/testing/stacktrace.js:462:15 > new at /hogehoge/google-closure-library/closure/goog/testing/asserts.js:1180:45 > Object.raiseException_ at /hogehoge/google-closure-library/closure/goog/testing/asserts.js:1154:9 > anonymous at /hogehoge/google-closure-library/closure/goog/testing/asserts.js:175:26 > anonymous at /hogehoge/google-closure-library/closure/goog/testing/asserts.js:373:3 > testGetHoge at /hogehoge/hoge_test.html:17:7 18:25:59.651 testSetHoge : PASSED 18:25:59.651 Done

どうやら testGetHoge のケースに失敗しているようです。

エラーの原因を特定する

上のメッセージを詳しく見てみると Expected (String) but was (unknown) とありますね。

hoge.get の戻り値は文字列型なはずなのに、よくわからない型のオブジェクトが返ってきているよ、という趣旨のメッセージであることがわかります。

そこで hogehoge.Hoge.prototype.get の内容をみてみると自身のプロパティ msg_ を返しているだけですから、どこかの時点で 'Hoge!' を代入し忘れたのかも、というように目星がつきます。

さて、さきほどの hogehoge.Hoge のコードには msg_ プロパティの初期値代入ミスがありました。

そこで、下のように修正してあげます。

hoge.js

// このモジュールはパブリックライセンス!/** * @fileoverview ほげほげなモジュール。 * @author あなたのメールアドレス (あなたの名前) */ goog.provide('hogehoge.Hoge'); /** * ほげほげなクラス。 * @constructor */ hogehoge.Hoge = function() { this.msg_ = 'Hoge!'; // おk! }; /** * メッセージを取得する。 * @return {string} メッセージ。 */ hogehoge.Hoge.prototype.get = function() { return this.msg_; }; /** * メッセージを設定する。 * @param {string} msg 設定したいメッセージ。 */ hogehoge.Hoge.prototype.set = function(msg) { this.msg_ = msg; };

すると、

Hogehoge testting [PASSED]/hogehoge/hoge_test.html 2 of 2 tests run in 3ms. 2 passed, 0 failed. 2 ms/test. 8 files loaded. . 18:45:49.839 Start 18:45:49.840 testGetHoge : PASSED 18:45:49.840 testSetHoge : PASSED 18:45:49.840 Done

テストに合格するようになりました。

このテストをするモジュール goog.testing.jsunit には JSUnit が使用されていますが、 Closure Library では非同期処理のテストや例外処理のテストといった機能を拡張しています。

アプリケーション・ライブラリレベル

複数のモジュールをまとめてテストする

ふつう、アプリケーション・ライブラリのリリース前にはアプリケーションに含まれるモジュールのすべてがテストに合格していなけれななりません。

しかしこのレベルになってくると、構成するモジュール・コンポーネントの数が多くなってきます。

それぞれのテスト用ファイルを個別に確認するのが負担となる場合は goog.testing.MultiTestRunner によってまとめてテストすることができます。公開前の最後の関門のひとつとしてもよいでしょう。

Closure Library 自体も MultiTestRunner によって全体をテストすることができるようになっています。

例を示します。 foobar.foo 、 foobar.bar という2つのモジュールからなるアプリケーションを考えます。

モジュールとそれぞれのテストファイルを用意します。

foo.js

goog.provide('foobar.Foo'); foobar.Foo = function() { this.msg_ = 'Foo'; }; foobar.Foo.prototype.get = function() { return this.msg_; };

foo_test.html

<!DOCTYPE HTML> <html lang="ja"> <head> <meta charset="UTF-8"> <title>Foo testting</title> <script src="/foobar/google-closure-library/closure/goog/base.js" type="text/javascript"></script> <script src="/foobar/foo.js" type="text/javascript"></script> <script type="text/javascript"> goog.require('goog.testing.jsunit'); goog.require('foobar.Foo'); </script> </head> <body> <script type="text/javascript"> function testGetFoo() { var hoge = new foobar.Foo(); assertEquals('Fooのget失敗', 'Foo', hoge.get()); } </script> </body> </html>

bar.js

goog.provide('foobar.Bar'); foobar.Bar = function() { this.msg_ = null; // <- 'Bar' がセットされていないといけない! }; foobar.Bar.prototype.get = function() { return this.msg_; };

bar_test.html

<!DOCTYPE HTML> <html lang="ja"> <head> <meta charset="UTF-8"> <title>Bar testting</title> <script src="../../google-closure-library/closure/goog/base.js" type="text/javascript"></script> <script src="bar.js" type="text/javascript"></script> <script type="text/javascript"> goog.require('goog.testing.jsunit'); goog.require('foobar.Bar'); </script> </head> <body> <script type="text/javascript"> function testGetBar() { var hoge = new foobar.Bar(); assertEquals('Barのget失敗', 'Bar', hoge.get()); } </script> </body> </html>

そして、 goog.testing.MultiTestRunner を動作させるテストページ foobar_test.html を用意します。

foobar_test.html

<!DOCTYPE HTML> <html lang="ja"> <head> <meta charset="UTF-8"> <title>Foo testting</title> <link rel="stylesheet" type="text/css" href="../../google-closure-library/closure/goog/css/multitestrunner.css"> <script src="../../google-closure-library/closure/goog/base.js" type="text/javascript"></script> <script type="text/javascript"> goog.require('goog.testing.MultiTestRunner'); </script> </head> <body> <div id="runner"></div> <script type="text/javascript"> var testRunner = new goog.testing.MultiTestRunner(); testRunner.addTests(['foo_test.html', 'bar_test.html']); testRunner.render(document.getElementById('runner')); </script> </body> </html>

次にブラウザで foobar_test.html を開き、 Start ボタンを押します。すると、 foobar.Bar.get にバグがあることがわかりました。

2 of 2 tests executed. 1 passed, 1 failed. Duration: 2s. Foo testting [PASSED] /hogehoge/closure-lib-wiki-materials/testing/foo_test.html 1 of 1 tests run in 1ms. 1 passed, 0 failed. 1 ms/test. 8 files loaded. Bar testting [FAILED] /hogehoge/closure-lib-wiki-materials/testing/bar_test.html 1 of 1 tests run in 4ms. 0 passed, 1 failed. 4 ms/test. 8 files loaded. ERROR in testGetBar Barのget失敗 Expected (String) but was > (unknown) > Object.get at /foobar/google-closure-library/closure/goog/testing/stacktrace.js:462:15 > new <anonymous> at /foobar/google-closure-library/closure/goog/testing/asserts.js:1174:45 > Object.raiseException_ at /foobar/google-closure-library/closure/goog/testing/asserts.js:1148:9 > anonymous at /foobar/google-closure-library/closure/goog/testing/asserts.js:169:26 > anonymous at /foobar/google-closure-library/closure/goog/testing/asserts.js:367:3 > testGetBar at /foobar/closure-lib-wiki-materials/testing/bar_test.html:17:7;

bar.js の 4 行目の null を 'Bar' と修正し、もう一度テストをしてみます。

2 of 2 tests executed. 2 passed, 0 failed. Duration: 2s. Foo testting [PASSED] /foobar/foo_test.html 1 of 1 tests run in 1ms. 1 passed, 0 failed. 1 ms/test. 8 files loaded. Bar testting [PASSED] /foobar/bar_test.html 1 of 1 tests run in 1ms. 1 passed, 0 failed. 1 ms/test. 8 files loaded.

すべてのテストに合格しました。