JavaScriptの品質保証 Part II – バグの発見
こんにちは、ゼノフィnakamuraです。
あるバグがあなたの組織の内外に広がると、バグを解決するコストが上がるという理論を以前の記事で検討しました。バグは常にワークスペースから逃げ出そうとしますので、同僚のワークスペースに感染し、最終的にお客様のブラウザに現れる可能性があります。バグは狡いものなので、我々開発者はそのバグを出来るだけ早く発見し、解決するために色々な技を適用する必要があります。また、最も難しい難題は、いかに時間やリソースを使用せずにそれを行うかという事なのです。
手動 vs 自動のテスト
ブラウザ上でいくつかのクリックをして、ある機能が動作しているかを手動で確認する、という選択肢はいつでもあります。これは開発者が自分でできますし、QAチームでもできます。通常ならば単一のテストサイクルにはこの姿勢をとるのが最も速いですが、このテスト方法は機能が増えるに連れてスケールしません。手動でテストする必要がある度にあなたの大切な時間が消費されます。商用ソフトウェア製品に関して言うと、出来るだけシステムを検証したほうが良いです:理想的なのは各コミットの後ですが、現実的には毎日一回程度となるでしょう。アプリケーションが大きくなるほど、テスト仕様が拡大し、更に時間を使うことになります。
機能が動作しているか確認するには、機能のユニットテストを作成するのがより良いテスト方法です。もちろん、このテストサイクルの取りかかりの段階では(特に一回だけ行う手動テストに比べると)多くの努力が必要です。しかし、一度作成するとそのテストは自動化されるので、余分なコストを避けながら、何度も起動させることができます。その上、このアプローチはスケールの調節が可能となります。下記のチャートを参照して下さい。最初の努力の後に緑の線が平になっています。そのテストは作成された時点から何度も再利用できます。
Bryntum社では、自動化されたテストにとても頼っていますので、このお陰で弊社の限られたリソース内で、我々の製品が全て良い状態を保つことができています。
いつテストを作成してバグと戦うべきですか?
さて、バグを発見したことにしましょう。しかし、これに対して新しいテストケースを作成するのが正当かどうか、どう判断しますか? “overtesting”(過度なテストを)しないように気をつけ、何をテストするべきかを選択し、それを優先させる必要があります。可能なユースケースを全てテストするのは非現実的であり、製品の開発から多くの時間を奪うでしょう。大きすぎるテストスイートも扱いにくくなりますので、完了するまでに長い時間がかかるでしょう。Bryntumでは、弊社の経験を通じて、新しいテストケースを作成するべきかを判断する質問があります。
- このバグは多勢のユーザーに影響しますか?
- このバグは以前にも報告されましたか?
- オンラインのサンプルバグに対して、このバグは売り上げに影響する可能性がありますか?
- テストは簡単に(10分以内)作成できますか?
- このバグの取り調べに、時間とエネルギーが多く使用されましたか?
このリストが全てではありませんし、また業界によって変化することもあるでしょう。
バグの発見
Part I の異なる段階について復習しましょう。製品のライフスタイルの中では、次の環境でバグを発見できます。
- あなたの開発コンピュータ
- あなたのチーム
- あなたの組織
- あなたのクライアント(あなたのような開発者)
- あなたのお客様のお客様:エンドユーザー
この各段階に関して、Bryntum社で利用しているソリューションについて検討しましょう。
Tier1 – あなたの開発機械。pre-commitのフック
最も早くバグを発見できる段階は自分のコンピュータ上です。きちんとしたテストスイートがあると、レポジトリにコードをコミットする前に、いくつかのスモークテストが実行できます。こうするとビルドが青信号のまま、開発者のモチベーションも保つことができます。開発者チームがモチベーションを失う大きな原因は壊れたビルドです。少なくともpre-commitは次のことを行うべきです:
- シンタックスが正確か検証すること。 JsHint を使うと良いです。
- 消し忘れた
debugger
やconsole.log
ステートメント、t.iit
テストステートメントを発見する(テストで簡単に忘れることがあります) - いくつかのスモークテストを実行する
スモークテストは最もシンプルなエラーを防ぐことに集中するべきで、コードベースの最もクリティカルな部分が大丈夫かどうかを確保するべきです。また、このスモークテストはとても素早く動作するべきです。そして、高度なUIテストを全て実行する場所ではありません。そうすると時間がかかりすぎます。 PhantomJS を利用して、全てのユニットテスト(DOMと関係ないもの)のサブセットといくつかのUIテストを実行します。概して、このpre-commitは約10秒かかりますので、我々のソースリポジトリを良い状態に整えます。次が現在のpre-commitのフックです:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | #!/bin/bash # 1. Run JsHint # ---------------------- jshint $(git diff --cached --name-only --diff-filter=ACMRTUXB | while IFS= read -r line; do if [[ $line =~ ^js\/.*\.js$ ]]; then echo $line fi done) if [ "$?" != "0" ]; then exit 1 fi echo "JSHint sanity test passed correctly" # passthrough the exit status of the failed pipe subshells set -o pipefail # 2. Look for forgotten t.iit statements in tests # ---------------------- git diff --cached --name-only --diff-filter=ACMRTUXB | while IFS= read -r line; do if [[ $line =~ ^tests\/.*\.t\.js$ ]]; then cat $line | grep -q '.iit(' if [ $? == "0" ]; then echo "t.iit() statement found in file: $line" # will exit a pipe-subshell only, need additional check below exit 1 fi fi done if [ "$?" != "0" ]; then exit 1 fi echo "No t.iit statements found" # 3. Run smoke tests # ---------------------- ../siesta/bin/phantomjs http://lh/ExtScheduler2.x/tests/index-no-ui.html?smoke=1 --pause 0 --no-color exit $? |
pre-commitフックの設定がセットアップできたら、みんながそれを利用する状態を確保するべきです。ビルドスクリプトに追加の手順を加えることで、.gitフォルダにpre-commitのファイルをコピーします。これにより、全ての開発者が同じフックを共有することが分かります。
1 2 | # install the pre-commit hook cp build/pre-commit .git/hooks/ |
Tier2:あなたのチーム。自動化されたテストスイート
pre-commitフックはあなたの開発コンピュータからバグが逃げ出せないようにする素晴らしいツールですが、バグを発見する最も良いツールは自動化されたテストスイートです。JavaScriptのコードベースをカバーするテストスイートは、対応している全てのブラウザ内で少なくとも毎日一回実行されるのが理想的です。Bryntumでは全ての製品をIE8/9/10, Chrome, Firefox, Safariで毎晩テストしています。朝に弊社の開発者が作業を始めると、既に最新の結果メールが受信トレイに入っています。壊れたテストは製品が壊れていることを示しているので、納品ができないため、弊社にとっては最優先となります。
Monkey love
上記で説明した通りに、バグは狡いものなので、色々な技を使って対策する必要があります。私のとても好きなテスト概念は モンキーテスト です。モンキーテストとはUIにランダムな入力をシミュレートして、その結果を監視することです。毎晩の Ext Scheduler ビルドの実行の一部として、各 45以上の例 をモンキーテストしています。この数年間はその繰り返しでたくさんのバグを無料で発見しました。そうです!無料で発見しました!!!
FOR FREE
これは弊社のようなリソースが限られた小企業にとって、とても役に立ちます。次は我々の例がモンキーに直撃されても、例外を生成せずに描画することを確保するテストとなります。
1 2 3 4 5 | t.waitForSelector('.sch-timetd', function() { t.pass('Scheduler example rendered without exception'); t.monkeyTest('>>schedulerpanel', 10); }); |
ここのROIは巨大です。ここにシンプルなt.monkeyTestステートメントを一つ書き込むと、バグハンターのモンキーが姿を見せます。これで充分満足できますよね?上記の行で “>> schedulerpanel” は Component Query が“schedulerpanel”に一致しているという意味で、二つ目の引数 (10) はランダムなアクション(クリック、タイプ、ドラッグなど)が何回実行されるべきか指定しています。
Tier 3:あなたの組織
モンキーテストはバグを無料で探索できるので、弊社のホームページを訪ねる人がライブの例を試すことも可能です。誰かがサンプルを所々クリックして、例外がスローされた場合、window.onerror
フックを利用して簡単にそれを知るとこができます。これは弊社の全てのオンラインサンプルにセットされてあるので、そのため無料でたくさんのバグが発見されました (しかし、モンキーよりもっと遅い段階で発見されました)。毎回オンラインの例で例外がスローされた時、次の情報が集められて、開発者にメールが送信されます:
1 2 3 4 5 6 7 8 9 10 11 12 13 | Message: Uncaught TypeError: Cannot call method 'disable' of null Url: http://www.bryntum.com/examples/gantt-latest/examples/advanced/js/Toolbar.js?_dc=1390056859111 Line: 12 Href: http://www.bryntum.com/examples/gantt-latest/examples/advanced/advanced.html Browser: Chrome32 Product version: 2.2.16 Local date: Sat Jan 18 2014 21:55:11 GMT+0700 (SE Asia Standard Time) |
上記でご覧の通り、エラーメッセージ、ブラウザ+バージョンが取得されて、その上に現地の日付けもあります:これは指定された時間帯に関係するバグだった場合とても役に立ちます。
Tier 4 and 5:あなたのクライアントとエンドユーザー
もしバグがこの段階まで辿り着いたなら、あなたがコントロールできない範囲に入っています。このようなバグの解決に対して、効率的で明確な過程が確保されていることがとても重要です。Bryntumでは、Assemblaのバグトラッカーツール: https://www.assembla.com/spaces/bryntum/support/tickets を利用しています。このツールで弊社がバグを報告した人と効率的に通信でき、お客様に対する弊社の透明性を高められます。
単純にバグを解決するだけは充分ではありません。お客様にパッチを送ることが同様に重要です。このため、高速な開発サイクルを使い、我々のリリースサイクルは2~3週間となっています。もしクリティカルなバグが発見されたら、それはフィックスされ(もちろんそれとともに新しいテストも行われ)、その上で新しいリリースに着手します。さらに、ナイティビルドも公開するので、バグが直された後にお客様がバグフィックスを検証することができます。
実際の話
レベル4で始まるBryntyumのバグライフサイクルの良くある例となります。
一、二週間ほど前に、ある状況での Ext Scheduler のタスクのリサイズに関する バグがレポート されました(チケットステータス:New)。 手動のテストを少し行った上、タスクの長さを変更して、ただマウスを数ピクセル動かしたら、リサイズ操作が不思議な動きをして、例外が発生しました。(チケットステータス:Accepted) Schedulerはリサイズ操作中のさまざまな段階でイベントを発火するので、正確にその操作の最終処理が行われなかったと予測しました。そして結果的にそれは正しい予測でした。このリサイズ機能は全てのユーザーに重要であり、テストするのは簡単だと思ったので、上記で説明した弊社のテストケース作成の条件の通りに、自分でテストを作成することに決めました。下記は5分以内に作成した、とてもシンプルなテストです:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | t.it('Should finalize if mouse only moves a little', function(t) { var scheduler = t.getScheduler({ renderTo : Ext.getBody(), height : 200 }); t.firesOnce(scheduler, 'beforeeventresize') t.firesOnce(scheduler, 'eventresizestart') t.firesOnce(scheduler, 'eventresizeend') t.chain( { waitFor : 'eventsToRender', args : scheduler }, { drag : '.sch-resizable-handle-end', by : [3, 0] } ); }); |
注:便利なgetScheduler
メソッドを利用しましたので、スケルトンテストコードを削減できました。このテストを実行すると、WebStorm出力ウィンドウに次のものが表示されました。
1 2 3 4 5 6 7 8 9 10 | run-phantom lh/ExtScheduler2.x/tests/index-no-ui.html --filter 062_resize.t.js --pause 0 fail 4 - Observable fired expected number of `eventresizeend` events Failed assertion `firesOk` at line 139 of event/062_resize.t.js?Ext=4.2.1 Actual number of events : 0 Expected number of events : 1 [FAIL] event/062_resize.t.js?Ext=4.2.2 3 passed, 1 failed assertions took 0.983s to complete |
これがeventresizeendはこのケースでは発火されてなかったという、必要とする証拠でした。我々のソースコードでこれに対するフィックスをすると、テストは青信号になり、問題を再生できませんでした(チケットステータス:Resolved)。
この時点から、もしこのバグがまた製品に現れたら、レベル2の段階でテストスイートが自動的に発見します。
Part IIは終わりです。
この記事でBryntumが高品質のソフトウェアを納品できるように、なるべく早くバグを発見するコツを紹介しました。次のシリーズでは継続的インテグレーションを紹介し、弊社のナイティビルドの説明をします。