JavaScriptでのテストや開発についてのアウトプット
最近JavaScriptを個人的に勉強しているんですが、そんなJS初心者ながら色々試すなかで気が付いた開発とかTDDとかについて色々思うところをアウトプットしてみようかと思います。
一番多いのは、ClientSideJSで、使ってるのはjQueryとQunitが中心でした。
でもこれからは別のフレームワークや、ServerSideJSなんかも出てきますし、
今読んでる本が終わったら、こっちの本も見てみたいと思っているので、
Test-Driven JavaScript Development: Safari Books Online
その前にこれを書いておこうという目的です。自分に付ける一つのTagという感じです。
あまり一貫性に拘らず、垂れ流したいと思います。
Ajax と API
以前こんな記事を書いたように、サーバ側がAPIでデータを提供し、ロジックをクライアント側に固めるタイプの開発方法を色々試してます。
万能では有りませんが、以下のような特徴が有ります。
- APIのみをインタフェースにしているため疎結合を保ちやすい。
- 生HTMLをいじるのでテンプレートエンジンの気持ち悪い記法が出てこない(JSPとか苦手です。。)
- ポータビリティが高い。(テンプレエンジン使ってないし、サーバ側はAPIが同じならどんな言語、どんなプラットフォームでも良い)
逆にトランザクションとか認証・セキュリティ周り、生産性等考えるべきところは多いんですが、それはまた別で考えます。
こうしたアプリはクライアント側で多くのロジックと表示、そしてAjaxによるデータのやり取りを実装することになります。
それらをTDDでやろうとすると、これまで他の言語(Server Side (Java|Python)とか)でやって来たこととは少し違う考え方が必要になる部分も多い気がします。
DOM周りと表示のテスト
クライアントサイドである時点で、表示を操作することは非常に多くなりますが、結論としては、「表示は分けて、テストはしない」が現実解のようです。
たとえばこんな関数があったとします。
$('#hoge').bind('click', function(e, data) { //ロジック var textdata = data.hoge; //表示処理 $('#area').text = textdata; };
何も考えないとこんな感じになってしまいますが、これは非常にテストがしにくいです。
表示はテスト出来ないというわけではなく、例えばSelenium等を用いれば、表示のテストは可能です。
こでは結果として画面上にtextdataが表示されているかを調べれることはできます。
しかし、それはここで開発を駆動するためにやろうとしている、「TDDの中でのJSのUnitTest」とはスコープが違うと考えます。
書きたいのは例えば、書いたJSのメソッドなりクラスなりのテストです。
すると、一番簡単なのは、このaの中にある二つの処理をわけることです。
つまり、こんなイメージ
var logic(data){ return data.hoge; }; var view(textdata){ $('#area').text = textdata; }; $('#hoge').bind('click', function(e, data) { //ロジック var textdata = logic(data); //表示処理 view(textdata); };
こうして、ロジックと表示の責務を分散して、テストの対象をlogicに絞ります。
logicはoutputがreturnなので非常にテストがしやすいです。
ではviewはどうするか。
処理の中に表示が出て来た場合、この動的な表示≒DOMの操作については、自分はjQueryを使ってそこに任せています。
これをテストするにはDOMのモックを作るか、何らかの方法で画面を見る必要が出ると思います。
しかし、自分はそこに時間をかけません。
きちんと表示されているかどうかは、jQueryの責務になります。そして、jQuery自体はjQueryの開発者によってテストされています。
ここで無理矢理テストを挟むことはjQuery自体をテストしようとしていることになると思います。
また、表示についてのテストは一般的にコストが高いです。また、表示はUIの変更をもろに受けるので、保守のコストが高くなりがちです。
そこを避けるために、本当に表示する処理だけが並んでいることを確認し、ある程度ブラウザから確認したらそこまでにします。
bind('click')についても同じで、#hogeをクリックしたときに正しくイベントが発火してコールバックが実行されているかは、jQueryの責務です。
まあ書いても良いんですが、ブラウザから見た挙動でも最初の内は確認出来てしまうので、あまり気にしすぎないで進めます。
ただし、ここでbindするイベントを、テストコードの中で$().trigger('eventname')で発火することで任意のタイミングで呼ぶには有効だと思うので、処理をカスタムイベントに固めることで得られる利点は有ると思います。しかし、そのカスタムイベントの中で表示とロジックが分かれているなら、自分はロジックだけをテストするので、そう言う場面にはまだ出会ったことは無いです。
カスタムイベントの有効利用は、自分としては今後の課題です。
いずれにせよ、他をテストしないで済む様にするために、できるだけlogicに処理を集めて、そこのテストに集中するというのが、一番現実的かなと思っています。
最終的にはライブラリをどんどん作って、それを徹底的にテストする感じでしょうか。
そして、logicの中にDOMが出てこなければ、DOMのMock無しでCIで回したりすることもできます。
まあ、ある種当たり前のことではありますが、今までテストを意識しないでJSを書いていた自分は、言うほどちゃんと出来ていないので、矯正していかないといけない大事な基礎だと思っています。
ただここで言ってるのはTDDの範囲の話なので、結合テスト等もっと上のレベルのテストで品質を意識する場合は、画面表示も含めたテストを書く必要はあると思います。
jQuery Object のテスト
DOMではなくjQueryオブジェクトとしてなら比較しやすい場合は多いです。
オブジェクトとして属性を参照した比較はこんな感じに出来ます。
例えば、jQueryオブジェクトのjqueryとselectorは使いやすいです。
これによって、「対象がjQuery Objectかどうか?」と「セレクタがなんだったか」を調べられます。
var param = '#template'; var $template = $(param); same($template.jquery, $().jquery); //それがjQueryオブジェクトかどうか same($template.selector, param); //#templateをセレクトしたものになっているか
探るとしても、この位までにした方がいいかなと、個人的には思っています。
Ajax のテスト
AjaxもjQueryを使ってやります。今までのちょっとしたものだと、圧倒的に「GETして表示」なパターンが多かったんですが、
アプリを組む場合は、RESTful APIを叩き、GET-POST-PUT-DELETE処理をすることも多くなります。
この辺を踏まえてどうやるのがいいか。実際難しくて色々試行錯誤している途中では有ります。
とりあえずQunitでasyncTestの方法を覚えた時点で、まあ何となくテストを書くことが出来る様にはなったんですが、
色々とつまずくことが多かったです。
その原因を考えるとこれも、先ほどと同じ
「テストしたいUnit(単体)はなんなのか」ってとこが大事なのかなという気がしてます。
例えば、簡単にgetJSONで以下のような処理を考えます。
$.getJSON(baseURI+'/hoge', {"foo":1, "bar":2}, function(res) { // res に対してのコールバック処理 });
多くのjQueryのサンプルでは大抵こんな感じに書いてありますが、
「これをテストする」なんて言っても一瞬止まってしまいます。
まず、inputとoutputを考えると、$.getJSON自体のinputはURL、パラメータ、コールバック関数です。
で、outputを期待するとしたら、コールバックが何かをすればそれがoutputと捉えることが出来るかもしれません。
もちろんコールバックの中の処理にもよりますが、典型的な例では
- return
- クラスの状態更新
- 表示
- イベントの発火
なんかがパッと浮かぶところですが、Ajaxベースのアプリだと、レスポンスの値を利用してさらに別のAjaxリクエストを呼ぶことも考えられます。
いずれにせよこれでは、何かを挟み込む余地が少ないので、色々切り出してしまいます。
var URI = baseURI + '/hoge'; var param = {"foo":1, "bar":2}; var callback = function(res) { // res に対してのコールバック処理 }; $.getJSON(URI, param, callback);
こうすると、色々テストしやすくなります。
で、じゃあ何をテストするかなんですが、例えば以下のようなものが浮かぶかもしれません。
- そもそもリクエストして値が取得できるかどうかのテスト。
- パラメータに対して、期待したレスポンスが返っているかどうかのテスト。
- 通信後の処理(コールバック)が仕様通りに行われるかのテスト
一つずつ、自分の考えで評価してみます。
そもそもリクエストしてレスポンスが取得できるかどうかのテスト
この場合は、実際にサーバに対してリクエストを投げるか、もしくはURLを差し替えてローカルパスに用意したJSONファイルなんかを叩くことで出来ます。
面倒くさいので雰囲気だけ
asyncTest('test request', function(){ var callback = function(res){ ok(res); }; setTimeout(fucntion(){ $.getJSON('./res.json', param, callback); }, 10); });
まあ、書いただけですね。
どういうことかというと、これは $.getJSON()自体をテストしている可能性があるということです。
URIとparamが間違っていなければ、データは取得出来るというjQueryの責務を信じれば、このテストは必要無いように思います。
パラメータに対して、期待したレスポンスが返っているかどうかのテスト。
面倒なんで書きませんが、これはもちろん正しいパラメータが渡っていれば、その先はサーバの責務です。
APIがしっかり定義出来ていれば、URIとパラメータの組み合わせに対して期待されるレスポンスについては分かっているはずです。
本来サーバの責務をクライアント側からテストするのは、「それ自体(APIのバグを出すとか)」が目的で無い限りは必要ないと考えると、これもいらないのかなと。
通信後の処理(コールバック)が仕様通りに行われるかのテスト
先ほどの例では、APIが決定しているなら、「この場合ちゃんと200が返ってくるか?」より、「200ならどうするか?」「404ならどうするか?」といった動作に着目したいということです。
それ自体はコールバックを切り出しておけば、それぞれの挙動に応じたレスポンスのデータをコールバックの引数に差し込めばできます。
差し込むためにわざわざJSONファイルを用意するような必要も有りません。
// callbackは以下の実装 var callback = function(res){ if(res){ //200 return res.msg; }else(res === {}){ //404 return false; } }; test('200',function(){ var res = { 'msg' : 'hello' } same(callback(res), 'hello'); }); test('404',function(){ var res = {} same(callback(res), false); });
ステータスコード周りも同じで、レスポンスの場合分けをキチンと考えておけば、APIそのものをテストしないで済むし、これが本来書いておきたいテストなのかなと思っています。
とは言うものの。。
こうしておくとAPIの変更に対してメンテナンスコストが低く保てる気がします。ただ、このコール時のURIやparamが正しい物かどうかはここでは見ていないことになります。
やはりもう1レイヤ上のテストくらいは書くことが多かったり。しかし、そう言うテストは結構変更に弱い気がしています。
その点については、小さいアプリならそもそも間違ってると動かないという一番分かりやすい形でフィードバックが得られるので、気がつくことが出来るかもしれませんが、規模が大きいとそうも行かない。
そういう場合は、この通信処理を含んだメソッドを定義して、もう一回り外側のテストを書くことになると思います。
通信処理を隠蔽したライブラリを作って、そのライブラリを用いた処理に対するテストになると思いますが、テスト方法はライブラリの設計(input-outputを何にするか等)に依存するので何とも言えません。
しかし、その「ライブラリ自体のテスト」は結局上の様になるんじゃないかなと思っています。
これが正しいのかは分かりませんが、今の時点ではそう言う感じかなと思っています。
この辺は自分がまだAjax周りのデザインパターンをきちんと学んでいないので、恐らくそれを勉強することで色々気づきが得られるかなと思っています。そろそろJavaScript自体には慣れてきたので、そこが次のステップかと。
Assertion
これは、先日行われたShibuya.jsで id:amachang さんが話されていた内容です。
そこにいたt-wadaさんもAssertionの有効性についてつぶやいていたと思うんですが、この具体的な実装方法が先日のamachangさんのエントリに書かれていました。それも非常に詳細に。
ユーザーインタフェースのようにテストしずらいコードでは、アサーションを使うとコードの質を効率よく改善できます。
この方法は、自分に今まで足りていなかった部分としてとても大事な技術だと思うので、試して行きたいです。
コンパイルとリント
ここまでの流れで書くテストは、実際には仕様とか振る舞いと言われる物のテストなんですが、実際に書いていくなかで、例えば振る舞いをテストするコードの振る舞いは合ってるのか?
みたいなところまで不安になり始めると、色々破綻してしまうのがテストの難しいところだと個人的には思っています。
つまり、そもそも変数名を間違ってたり、構文がミスしていたり。
そういうケアレスなミスっは、もちろんテストによってあぶり出せるかもしれませんが、そういのが原因でテストが落ちたとき、記述ミスがテスト側に有ったりするのは精神衛生上よくありません(自分の場合)。
まあ、なんというか Java on Eclipse な開発をしていたらあまり感じないこの現状は、JavaScriptという割とゆるふわな言語で、キラーなIDEがJava程整っていないところも有るかもしれません。(自分はEmacsのJS2-modeでやってます)
しかし、Java程でなくとも基礎的な部分は色々なツールに頼れます。
Web上のツールもあります。JSONLint - The JSON Validator.とか、http://www.jslint.com/
それ以外に最近出て来たツールとして以下のような物も有ります。
StyleGuideとチェッカとコンパイラ
ソースがちゃんと一定の規則で書かれていることはJavaScriptに限らず重要です。
そこで、スタイルガイド(構文規約)等を作るのは、例え1人で開発するにしても有益だと思います。
このスタイルガイドは自分で作っても良いんですが、自分は俺々規約ではなくGoogleのStyleGuideを取り入れることにしてます。
理由は、Googleが練り上げたもので、規約を読んでても納得どころか勉強になる点が多かったからというのもあります。
しかし、いきなり覚えようと思っても無理なので、ツールを使います。
規約に合っているかをチェックしてくれるlinterが提供されています。
How to Use Closure Linter - Closure Tools — Google Developers
使い方は簡単で、導入もeasy_installですぐ入ります。
ここにも記事が有りましたが、ここで指摘しているバグはもう修正されているようです。
実際に手元に有った適当なJSをチェックしてみます。
gjslint
以下がbefore
// db.js var db ={ set : function(key, obj){ localStorage.setItem(key, JSON.stringify(obj)); }, get : function(key){ var tmp = localStorage.getItem(key); return (tmp === undefined)? null : JSON.parse(tmp); }, each : function(fun){ try{ for (var i=0; i<localStorage.length; i++){ var k = localStorage.key(i); fun(k,db.get(k)); } }catch(e){ for (var key in localStorage){ if(key === 'key') continue; fun(k,db.get(k)); } } }, del : function(key){ delete localStorage[key]; } };
結果は以下。
% gjslint db.js ----- FILE : /path/to/db.js ----- Line 1, E:0002: Missing space after "=" Line 2, E:0005: Illegal tab in whitespace before "set" Line 2, E:0001: Extra space before ":" Line 2, E:0002: Missing space before "{" Line 3, E:0005: Illegal tab in whitespace before "localStorage.setItem" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Line 26, E:0005: Illegal tab in whitespace before "delete" Line 27, E:0005: Illegal tab in whitespace before "}" Found 47 errors, including 0 new errors, in 1 files (0 files OK). Some of the errors reported by GJsLint may be auto-fixable using the script fixjsstyle. Please double check any changes it makes and report any bugs. The script can be run by executing: fixjsstyle db.js
でこれはチェックだけですが、最後に有るようにこれらを修正までしてくれるツールとして、
fixjsstyle もあります。使い方は同じ。
fixjsstyle
以下がafter
var db = { set: function(key, obj) { localStorage.setItem(key, JSON.stringify(obj)); }, get: function(key) { var tmp = localStorage.getItem(key); return (tmp === undefined) ? null : JSON.parse(tmp); }, each: function(fun) { try { for (var i = 0; i < localStorage.length; i++) { var k = localStorage.key(i); fun(k, db.get(k)); } }catch (e) { for (var key in localStorage) { if (key === 'key') continue; fun(k, db.get(k)); } } }, del: function(key) { delete localStorage[key]; } };
この場合主にスペースで怒られて、修正されています。
別の例だと、1行が80文字以上だったり、セミコロン抜けもチェック出来ます。
あと、Googleの規約ではJSDocを書いてそれをClosureコンパイラでコンパイル時にチェックする、というようなこともするので、
JSDocについてもチェックすることができるようです。
(自分はまだそこまでやったことは無いんですが。)
GoogleClosureCompiler
最後にコンパイラです。
ここではGoogle謹製のJavaScriptライブラリの一部である、ClosureCompilerを紹介します。
Closure Compiler Documentation - Closure Tools — Google Developers
このコンパイラは、よくあるコード圧縮ツールの用にも使えますが、使わないメソッドの削除、構文ミスチェックなどもしてくれます。
そして、コンパイラはJSDocでアノテーションを記述すると、コンパイル時に型チェックの様なことをしてくれます。
「JSなのにコンパイルすんのかか?」みたいなことを言われても困りますが、Google並みに規模の大きいサービスのJS開発は、こういうのが無いとたち行かないんだろうと考えれば納得の仕組みかと思います。
また、コンパイル時の指定によっては、インライン展開による最適化をしてくれます。
例えば
function hello(name) { alert('Hello, ' + name); } hello('New user');
が
alert("Hello, New user");
になるということです。
ここまでやるとなると不安になる部分も有りますが、まあ速くはなるでしょう。
複数で開発するような場合なんかは特に、自分で中途半端な最適化をしたりせず、極力丁寧に書き込んで最適化はコンパイラに任せるという進め方ができると思います。
インストールしなくてもここから試せます。
なぜWeb上で試せるのかというと、どうもこのツールはWebサービスとして提供されているようです。
Getting Started with the API - Closure Tools — Google Developers
ClosureはJavaScriptのUIライブラリ等も提供されているんですが、自分はまだそちらは使ったことがありません。
コンパイラもまだ簡単なところしか使ってないですが、ちょっとづつ導入して色々試して行きたいと思っています。
ECMAScript5
JavaScriptの仕様にあたるECMAScript5の策定が進み、実装されればJavaScriptはかなり機能強化される予定です。
ECMAScript5は今までのJSの弱点の補完やServerSideJSも視野に入れて策定されているようなので、普及すればJSの開発スタイルも大きく変わりそうです。
これに伴い、JavaScript開発のバイブル「サイ本」の第6版が現在執筆中で、今のところこれが一番詳しそうです。
Rough Cutsなら手に入ります。
JavaScript: The Definitive Guide, 6th Edition: Safari Books Online
今からでも少しづつ見ておくと良いかと思います。
mock としてのファイルとlocalStorage
ここは、テストというか開発の話です。
サーバを使わずにAjaxな処理の挙動を確認するには、ファイルにレスポンスとなるデータを記述する方法があります。
例えば期待されるJSONやXMLを直接書き込んだファイルを用意して、AjaxのURLをそのファイルへのローカルパスにする方法です。
これはテストの時には使えるかもしれませんが、開発において実際のアプリケーションの流れを再現するためには使えないので、
UIの開発は開発サーバを用意してAjax処理もそのサーバのAPIを叩いてやることになる場合は多いと思います。
ただ、クライアントの開発をするのに開発サーバをいちいち起動するのは面倒ですし、開発のために仮APIをsinatraやらflaskやらで組むこともできますが、それも手間です。
CouchDBという手も有るんですが、実際やるとAPIがCouchDBに多少なりともするという問題も有ります。
そこで、この開発サーバのモックとして、localStorageを使えないかと思って試しています。
つまり、クライアント開発時に
$.post(url,data,callback);
ではなく
lib.post(url,data,callback);
というように1枚上に被せます。
このlibライブラリのpostメソッドは、開発時はlocalStorageに対して、本番は本番サーバに対してアクセスする様に実装します。
var lib = { post : //本番サーバへ } var mock = { post : //localStorageへ } lib = mock//本番はこれを消す $(function(){ lib.post //書き換えてるので実はmock });
上書きの方法はもっとかっこ良くやれば良いですが、とにかく切り分ける様にしておきます。
mockは本番は依存解決時に外すか、minifyするときにごっそり消すのでも良いと思います。
なので、先ほども書いた様に、実際のAPIに対するライブラリを書いて、それを徹底的にテストします。
そして、mockもそのテストを全て通す様に実装します。
するとUI側の開発時はAPIさえ決まっていれば、サーバ無しで実装を進められます。
実装中のhtmlはhttp://じゃなくてfile://で開いて開発出来ます。
もちろん、これはまだアイデアの段階なので欠点は山ほどあります。
- localStorageが無いとダメ(uupaa.js使えばいける)
- FireFoxはfile://で開いたファイルのlocalStorageの内容を更新時にクリアする。bugzilla,ここも
- 現状のlocalStorageは文字列しか入らない(JSON.parse,JSON.stringifyを使ったラッパーを書く)
- 出し入れが同期処理(workerとsetTimeoutでバックグラウンド通信してるっぽくするか、indexed dbが実装されれば非同期APIがあるかも)
- ステータスコードは?(エラーは疑似発生させるしか無い)
- メンテナンスコストが高い(これはクライアント開発の手間とのトレードオフか)
APIによってクライアントとサーバを疎結合にしたということは、実際の開発はAPIを取り決めとして定義したあと、クライアントサイド、サーバサイドの開発が並行して進む可能性があります。
その場合は、中途半端なファイルモックをメンテし続けるよりメリットがある場合も多いかと思います。
localStorage自体の実装差は結構有るので、事前にlocalStorageの挙動自体をきちんと把握する必要が有りますが、これは一回やれば良い。もしくは信頼できるライブラリを入れることで解決できます。
そうすると、個々のメソッドのテストにも使えるし、GET-POST-PUT-DELETEを駆使したクライアントの開発も、いちいちサーバに繋がずに動きを確認出来ます。
まあ、ある程度進んだら開発サーバに繋いだ方がいいですが、そうして見つかるのは割とmock自身のバグだったりします。
これを辛抱強く繰り返せば、mockの完成度は高くなるので、機能拡張や逆にAPI追加のプロトタイプとして使えるかもしれません。
欲を出せば、このmockで被せるAPIをRESTfulにすることを意識しておけば、その後別のアプリを開発する時にAPIをRESTfulにすることで、mockはある程度使い回しが効くかもしれません。
統一的インタフェースに被せただけのインタフェースである以上、そうでないとおかしいはず。
あと、現時点で現実的なクライアントサイドストレージはlocalStorageですが、色々やろうとするとindexed Databaseの実装が欲しいところです(非同期とか)。
まあ、ここまでくると妄想ですが。
一応やっては見ました。
localStorageをmockにしてjsonengineのクライアントを開発する - Block Rockin’ Codes
localStorageの挙動と簡単なラッパー - Block Rockin’ Codes
これはもう少し続けてみたいと思っています。
(なんども言いますが完璧ではないです。あくまでアイデア。)
まとめ
なんというか、JSの開発については他の言語より難しいと感じる部分が自分として多く、色々試してるんですが中々次のステップに進め無い感じがあるので、とりあえず色々アウトプットすることで整理しようと思い書き始めました。
ここを起点にもう一度色々見直したいと思います。