タイトルは「テストとカバレッジが必要だ」ではなく、「Require.jsとテストとカバレッジ」のお話です 😛
ここしばらくの試行錯誤によって、Require.jsを使ってWebアプリを実装してテストとコードカバレッジを計測するやり方が手元で整ってきたので、それをご紹介したいと思います。
ここではサンプルとして、「JSONで書いた設定ファイルのメッセージをブラウザで表示する」というアプリを作ってみます。
アプリ本体では、以下のライブラリを使います。
まずはアプリのファイルツリーを決めましょう。今回は以下のようにします。
. ├── index.html ├── scripts │ ├── main.js │ └── message.js └── vendor ├── jquery-2.1.3.min.js └── require-2.1.17.min.js
実際のファイルをGithubに置いておきましたので、以下の要領で(ほぼ)同じ物をお手元に持ってこられます(コマンド実行時の細かい出力は省略しています)。
kuroda@charlie:~/tmp$ git clone https://github.com/hiraku/js-bdd-example.git Cloning into 'js-bdd-example'... kuroda@charlie:~/tmp$ cd js-bdd-example/ kuroda@charlie:~/tmp/js-bdd-example$ git checkout 0095c9f4 Note: checking out '0095c9f4'. kuroda@charlie:~/tmp/js-bdd-example$ tree . ├── README.md ├── index.html ├── scripts │ ├── main.js │ └── message.js └── vendor ├── jquery-2.1.3.min.js └── require-2.1.17.min.js
ライブラリをご自分で用意する場合は、それぞれ以下のサイトからダウンロードしてください。
アプリの画面になる index.html
は、次のとおりです。
<!DOCTYPE html> <html> <head> <title>Example</title> <script data-main="scripts/main" src="vendor/require-2.1.17.min.js"></script> </head> <body> <h1 class="message"></h1> </body> </html>
Require.jsを使うので、基本的に script
要素は以下の1つだけになります。
<script src="vendor/require-2.1.17.min.js" data-main="scripts/main"></script>
data-main
属性には、最初にロードする自前のスクリプトを、末尾の .js
を除いて書くことで指定します。今回は scripts/main.js
なので scripts/main
と書きます。
その scripts/main.js
はひとまず以下のように書いてみました。
require.config({ paths: { jquery: "../vendor/jquery-2.1.3.min" } }); require(["jquery", "message"], function($, Message){ });
RequireJSの require
関数や define
関数で使用したいモジュールを指定すると、さきほどの data-main
属性で指定したjsファイルのあるディレクトリを基準ディレクトリにして探します。今回だと scripts/
の下から探します。
指定するモジュールの名前は、jsファイルの名前から末尾の .js
を除いたものになります。
モジュール名には "foo/bar/module-name"
とかのサブディレクトリも使えます。
require.config
関数の paths
で特定のモジュール(ライブラリ)だけ別の場所のファイルを使うように指定することもできます。
paths
でも、実際のjsファイル名から.jsを除いたものを指定します。
require
や define
関数でモジュール名ではなく.js付きのファイル名をそのまま書くと、呼び出し元のhtmlがあるディレクトリを基準に探します。
例えば今回の例だと、 "jquery"
の代わりに "vendor/jquery-2.1.3.min.js"
と指定することもできます。
require
関数に渡すコールバック関数の中にアプリの処理を書きます。コールバックの引数には、1つ目の引数で指定したモジュールが入ってきます。
上の例では reuqire
関数で "message"
というモジュールも指定していますが、これが実際にメッセージを設定ファイルから読んで指定した要素に書き出すモジュールで、これから作っていくものになります。
とりあえず以下のように空のモジュールを書いて scripts/message.js として保存します。
define(["jquery"], function($){ console.log("in message.js", $("body").jquery); });
とはいえ、本当に空っぽだとちゃんとロードされたか分からないので、 console.log
でjQueryのバージョン番号を出力するようにしてみました。
この段階で index.html
をブラウザで開くと、開発コンソールに
in message.js 2.1.3
と表示されるはずです。
それでは改めて、 Message
モジュールの機能を検討します。
今回は
というアプリなので、以下の関数があれば良さそうです。
message.get()
: 設定ファイルからロードしたメッセージを this.text
に格納するmessage.put(element)
: 引数に与えた element
(jQueryの要素オブジェクト)の中にthis.text
を element.text()
で書き出す今度はテスト関係のファイルツリーを決めましょう。
テストには以下のライブラリ・フレームワークを使います。
テスト周りを含めたファイルツリーは以下のようにします。
. ├── README.md ├── index.html ├── scripts │ ├── main.js │ └── message.js ├── test │ ├── index.html │ ├── main.js │ ├── message-spec.js │ └── vendor │ ├── chai │ ├── mocha │ └── sinon-1.14.1.js └── vendor ├── jquery-2.1.3.min.js └── require-2.1.17.min.js
test/index.html
で最初の test/main.js
などのロードと結果の表示を行い、 test/main.js
からRequireJSの仕組みでテストスクリプトをロードします。
それぞれのテストスクリプトはテスト対象のモジュール名から test/XXXX-spec.js
とします(今回は test/message-spec.js
だけ)。
というわけで、まずはテスト用の test/index.html
(テストランナーと呼んでいます)ですが、次のようにします。
<!DOCTYPE html> <html> <head> <title>Test runner</title> <link rel="stylesheet" href="vendor/mocha/mocha.css" /> <script src="vendor/mocha/mocha.js"></script> <script data-main="main" src="../vendor/require-2.1.17.min.js"></script> </head> <body> <div id="mocha"></div> </body> </html>
ここでは mocha.js
を、RequireJSとは別に script
要素でロードしています。
テストだけの場合だと、 mocha.js
もRequireJSの枠組みでロードして構わないのですが、後で説明するカバレッジ計測の際に、RequireJSでロードしてしまうと正しくテストが実行されないので今からこのようにしています。
test/main.js
はこんな感じになります。
require.config({ baseUrl: "../scripts", paths: { chai: "../test/vendor/chai/chai", sinon: "../test/vendor/sinon-1.14.1", jquery: "../vendor/jquery-2.1.3.min" } }); define("mocha", function(){ window.mocha.setup("bdd"); return window.mocha; }); define("expect", ["chai"], function(chai){ return chai.expect; }); require(["mocha", "message-spec.js"], function(mocha){ mocha.run(); });
先ほどのアプリ用の scripts/main.js
と比べて、いくつか違う部分があります。
まず、 require
とかdefine
関数で指定するモジュール(jsファイル)を探す場所が、テスト用の test/main.js
がある場所ではなくアプリ用の scripts/main.js
の場所になってほしいので、 require.config
の baseUrl
オプションで指定しています。
baseUrl: "../scripts",
../scripts
と相対パス指定で書いていますが、この時だけ基準がテスト用の test/main.js
があるディレクトリになります。 paths
の基準ディレクトリは baseUrl
で指定した scripts/
になります。
つぎに、先ほどMochaをRequireJSの外でロードしましたが、RequireJSの中からも require
や define
で参照できるようにモジュールを別途定義しています。
define("mocha", function(){ window.mocha.setup("bdd"); return window.mocha; });
グローバル変数の mocha
(実際は window
オブジェクトの mocha
プロパティ)を define
から返すことで、
require(["mocha"], function(mocha){...});
といった具合に参照できるようになります。これがなくてもグローバル変数で参照できてしまいますが、他のモジュールと統一した方法で使えた方が気分が良いので..。
また、この時ついでに mocha.setup()
関数で「BDD形式でテストを書く」と宣言しています。
その次の define
関数では、 chai.expect
を直接参照できるようにダミー的なモジュールを宣言しています。
define("expect", ["chai"], function(chai){ return chai.expect; });
これにより、
require(["expect"], function(expect){
という書き方で直接 expect
を参照して使えるようになります。
require
関数では "mocha"
とテストスクリプトの "message-spec.js"
を参照することでRequireJSにロードさせて、テストの開始を行っています。
require(["mocha", "message-spec.js"], function(mocha){ mocha.run(); });
"message-spec.js"
は末尾に .js
を残した形で指定したので、RequireJSは test/main.js
のある場所からファイルを探して、 test/message-spec.js
をロードします。
テストスクリプトの雛形はこんな風になります。
// RequireJSのdefine関数でテストフレームワークのモジュール、共通のライブラリと、 // テスト対象のモジュールを参照する define(["expect", "sinon", "jquery", "message"], function(expect, sinon, $, Message){ // describe関数でテスト全体を包む describe("Message", function(){ // 必要に応じてbeforeEachとafterEach(またはbeforeとafter)で下準備・後片付けを入れる beforeEach(function(){ }); afterEach(function(){ }); // it関数にテストを書く it("XXX", function(done){ }); }); });
実際にテストを書くと、 test/message-spec.js
はこうなります。
define(["expect", "sinon", "jquery", "message"], function(expect, sinon, $, Message){ describe("Message", function(){ // describeの中を通して効く変数を用意する var message; beforeEach(function(){ // jQuery.ajaxの代わりに、事前に作った文字列を与える処理をsino.stubで用意します。 // アプリ内のファイルを参照するのでこのケースではstubではなくても構わないのですが、 // 今回は試しにsinon.stubを使います var d = $.Deferred(); sinon.stub($, "ajax").returns(d.promise()); d.resolve({message: "Dummy message"}); message = new Message("../conf.json"); }); afterEach(function(){ // stubの$.ajaxの後片付け $.ajax.restore(); }); // 非同期な処理の場合はitのcallbackの引数にdoneを取る it("get", function(done){ // message.get関数のdoneの中で文字列の判定を行う message.get().done(function(text){ // doneのコールバック関数の引数に設定ファイルからのメッセージ文字列が入るか調べる expect(text).to.equal("Dummy message"); // doneの時点で message.text にメッセージ文字列が入っているか調べる expect(message.text).to.equal("Dummy message"); // 非同期処理の終わりでdone()を呼ぶ done(); }); }); // message.putは非同期ではないのでdoneは使わない it("put", function(){ var e = $(""); message.text = "foo"; message.put(e); expect(e.text()).to.eq("foo"); // itの中に直接、判定処理を書く }); }); });
この時点で一度テストを実行してみましょう。
といっても、単に test/index.html
をブラウザで開くだけです。
scripts/message.js
の中に雛形しかないので、「Message関数なんてありません(意訳)」といってきています。
というわけで、 Message
関数(オブジェクトのコンストラクタ)と、 message.get
と message.put
(インスタンスのプロパティ関数)を用意しましょう。最初はそれぞれ空にしておきます。
define(["jquery"], function($){ function Message(confUrl){ } Message.prototype.get = function(){ var deferred = $.Deferred(); deferred.resolve(); return deferred.promise(); }; Message.prototype.put = function(element){ }; return Message; });
空にすると言ったばかりですが、message.get
は jQuery.Deferred
の promise()
を返すようにしておきます。本当に空にしていたらどうなるかはお手元で試してみてください。
この状態で再度テストを実行すると、今度は以下のようになります。
先ほどのテスト結果は beforeEach
の中で Message
がないと言ってきていましたが、今度は get
と put
のテストの中で、期待した文字列が得られなかったと言ってきました。
それではいよいよ、きちんとした実装を書いていきます。
まず、コンストラクタ Message()
で設定ファイルのURLを保存します
function Message(confUrl){ this.confUrl = confUrl; }
message.get
関数は jQuery.get
で得られたJSONデータから message
を取り出して保存しつつ、 done
のコールバックにも渡るようにします。
Message.prototype.get = function(){ var deferred = $.Deferred(); var self = this; $.get(this.confUrl).done(function(data){ self.text = data.message; deferred.resolve(self.text); }); return deferred.promise(); };
message.put
関数はjQueryの要素オブジェクトを引数に取って、その text
にメッセージ文字列を設定します。
Message.prototype.put = function(element){ element.text(this.text); };
これでテストを動かすと、晴れて passes:2
となります。
最後にこの Message
オブジェクトを scripts/main.js
の require
の中で使ってアプリを仕上げます。
require(["jquery", "message"], function($, Message){ var message = new Message("conf.json"); message.get().done(function(text){ $(".message").text(text); }); });
設定ファイル conf.json
も用意しておきます。
{ "message": "Hello, world" }
これでアプリのindex.html
をブラウザで開けば、アプリが動いてくれます。
それではいよいよ、コードカバレッジを計ってみましょう。以下のライブラリを使います。
まず、 Blanket.js をgithubからcloneします。
test/vendor$ git clone https://github.com/alex-seville/blanket.git Cloning into 'blanket'... remote: Counting objects: 5880, done. remote: Total 5880 (delta 0), reused 0 (delta 0), pack-reused 5880 Receiving objects: 100% (5880/5880), 6.11 MiB | 3.34 MiB/s, done. Resolving deltas: 100% (2449/2449), done. Checking connectivity... done.
実際に使うのは https://github.com/alex-seville/blanket/tree/master/dist/mocha にある blanket_mocha.js だけなので、これを test/vendor/blanket/dist/mocha/blanket_mocha.js
としてダウンロードしても構いません。
ただし、 http://blanketjs.org/ の”Download 1.1.5″からダウンロードできるファイルは古くて以下で紹介する方法では使えないので、その点はご注意ください(本稿執筆時点の情報です。1.1.6とかになったら、大丈夫かもしれません)。
カバレッジ計測には、以下の test/coverage.html
のような専用のテストランナーを用意します。
<!DOCTYPE html> <html> <head> <title>Code coverage</title> <link rel="stylesheet" href="vendor/mocha/mocha.css" /> <script src="vendor/mocha/mocha.js"></script> <script data-main="main" src="../vendor/require-2.1.17.min.js"></script> <script data-cover-never="vendor" src="vendor/blanket/dist/mocha/blanket_mocha.js"></script> </head> <body> <div id="mocha"></div> </body> </html>
通常のテストと違うのは、blanket_mocha.js
をロードする script
要素を追加した点です。
このとき、 data-cover-never
属性でカバレッジ計測から除外するディレクトリを指定します。今回の例では外部ライブラリなどを入れた vendor
ディレクトリを除外するように指定しています。
この指定で vendor/
と test/vendor/
の両方が除外されます。
カバレッジ計測の準備はこれだけです。 test/coverage.html
をブラウザで開くと、画面下部にカバレッジ計測の結果が表示されます。
今回は規模が小さいし、きちんとテストを書きながら開発を進めたので、コードカバレッジは見事に満点になっています!
試しに scripts/message.js
にテストされない関数を追加してみましょう。
Message.prototype.hoge = function(){ alert("HOGE!"); };
coverage.html
をリロードすると、きちんとカバレッジが低下しています。
alert
を10行くらい繰り返すとさらに低下して、60%を下回ると表示が赤くなります。
ちなみに計測結果のファイル名をクリックすると、どの部分がテスト時に実行されなかったかが分かるようになっています。
テストの方にも実行されないコードを追加してみましょう。
describe("hoge", function(){ var alert; beforeEach(function(){ alert = sinon.stub(window, "alert"); }); afterEach(function(){ window.alert.restore(); }); it("hoge", function(){ function testHoge(){ message.hoge(); expect(alert.call_edOnce).to.be.ok; }; }); });
かなり苦しいですが、状況を変えて同じテストを試すために関数化したとか、そんな場面だと思ってください。
この状態で coverage.html
を開くと、これまたきちんと test/message-spec.js
のカバレッジが低下しています。
テスト対象のカバレッジが満点じゃないのは単にテストが不足しているからですが、テストその物のカバレッジが満点でない場合は、実行されると思っているテストが実行されていないわけで、非常に良くないです。
というわけで、テストコードだけのカバレッジを見られるようにしておくと便利なので、次のような test/coverage-only-test.html
も用意しておきます。
<!DOCTYPE html> <html> <head> <title>Test coverage</title> <link rel="stylesheet" href="vendor/mocha/mocha.css" /> <script src="vendor/mocha/mocha.js"></script> <script data-main="main" src="../vendor/require-2.1.17.min.js"></script> <script data-cover-never="[vendor,../scripts]" src="vendor/blanket/dist/mocha/blanket_mocha.js"></script> </head> <body> <div id="mocha"></div> </body> </html>
最初の test/coverage.html
と違うのは、 blanket_mocha.js
をロードする script
要素の data-cover-never
属性で vendor
と ../scripts
の2つを除外するよう指定しているところです。
複数のパスを除外する場合は [...]
で囲んでカンマ( ,
)で区切ります。
<script data-cover-never="[vendor,../scripts]" src="vendor/blanket/dist/mocha/blanket_mocha.js"></script>
これで、 test/
以下のjsだけのカバレッジを計測して表示できます。
あとは、それぞれのテストランナーを相互にリンクしたりするのも便利です。
というわけで、私たちにはテストのカバレッジが必要です(!?)
どっとはらい