Firefoxアドオン開発者向け自動テストツールのUxUは、新たに発見したバグの修正にも活用することができます。本日リリースされたXUL/Migemo バージョン0.11.7で行われた修正の場合を例に、実際のデバッグ作業の流れを解説します。
状況
XUL/Migemoは、Firefoxで表示しているページ内のテキストを検索する機能を提供するアドオンですが、検索を開始する際に、「現在のスクロール位置から検索を開始する」という処理を含んでいます。0.11.6以前では、この機能を使用している時に、ページ先頭から検索が始まるべき場面で、先頭以外の場所から検索が始まってしまうことがあるという問題が起こっていました。
再現条件の特定
いくつか条件を変えて調査した結果、スクロールが発生しているページでは期待通りの結果になっているのに対して、スクロールが全く発生していないページ(ページ全体がウィンドウの現在の大きさの中に収まっているページ)では期待と異なる結果になっていることが判明しました。
原因箇所の特定
前述の処理の肝となっているのは、pXMigemoFindクラスのfindFirstVisibleNode()
というメソッドです。このメソッドは、渡されたフレーム(DOMWindow)において現在のスクロール位置で見えている最初の要素を検索して返す物です。このメソッドの戻り値を確認した所、前述の条件下では戻り値が期待と異なっている事が判明しました。
このことから、今回主な修正対象になるのはこのfindFirstVisibleNode()
というメソッドであるということになります。
テストケースの作成
上記メソッドの実装を見直す前に、UxU用のテストケースを作成します。これにより、これから行う修正で目指すべきゴールが明確になります。つまり、このテストが成功する状況まで持って行くことが、今回の修正のゴールとなります。
テストケースはJavaScriptのコードだけで完結する場合もありますが、今回のような場合は実際のWebページを使ってテストを行う必要があります。そこで、問題が発生する条件と発生しない条件の両方の事例としてHTMLドキュメントを用意します。
- スクロールが発生しないページ(shortPage.html):ページの内容が短いため、スクロールが発生しません。
- スクロールが発生するページ(longPage.html):ページの内容がある程度長いため、ウィンドウサイズによってはスクロールが発生します。
これらのドキュメントを使い、shortPage.htmlではスクロールが発生せずlongPage.htmlではスクロールが発生する、という条件の下でテストを行うテストケースをこれから作成することになります。
ところで、現在の実装で問題が起こっている場合だけでなく、すでに正常に動いている場合の事例も同時に作成していることに気がついたでしょうか? 両方の場合を常にテストすることで、「ある問題を修正したら、今度は、今までは正常に動いていた物が動かなくなった」という状況、いわゆる後退バグを未然に防ぐことができます。 1
それではテストケースを作成します。
utils.include('pXMigemoClasses.inc.js');
var findModule;
function setUp()
{
yield utils.setUpTestWindow();
findModule = new pXMigemoFind();
findModule.target = utils.getTestWindow().gBrowser;
}
function tearDown()
{
findModule.destroy();
findModule = null;
utils.tearDownTestWindow();
}
function testFindFirstVisibleNode()
{
// ここに実際のテスト内容を記述する
}
pXMigemoFindクラスの単体テストはまだ作成されていなかったので、今回はsetUpとtearDownから作成します。すでに作成済みのテストケースがあり、それにテスト項目を追加する場合、この作業は不要となります。
pXMigemoFindクラスはtabbrowser要素を用いて初期化する必要があるため、setUpでテスト用のFirefoxウィンドウを開き、そのウィンドウのtabbrowser要素で初期化します。また、tearDownではsetUpで開いたテスト用のFirefoxウィンドウを閉じて、pXMigemoFindクラスのインスタンスを破棄します。UxUは関数名を見て自動的にその種類を判別するため、これだけで、これらの関数はテストの初期化処理と終了処理として認識されるようになります。
なお、インクルードしているpXMigemoClasses.inc.jsというファイルは、pXMigemoFindクラスやそのクラスが依存しているすべての関連クラスの定義を読み込む物です。
次に、テストの内容を作成していきます。
function testFindFirstVisibleNode()
{
var win = utils.getTestWindow();
win.resizeTo(500, 500);
assert.compare(200, '<', utils.contentWindow.innerHeight);
// ここに実際のテスト内容を記述する
}
関数名を「test」で始めると、その関数はテストの内容として自動的に認識されます。
最初に、ウィンドウの大きさを調整して、「shortPage.htmlではスクロールが発生せずlongPage.htmlではスクロールが発生する」という条件を整えておきます。ここでは、テスト自体が期待通りの条件下で実行されていることを確認するために、assert.compare()
でテスト用フレームの大きさを調べています。
yield utils.addTab(baseURL+'../res/shortPage.html', { selected : true });
var frame = utils.contentWindow;
var node = findModule.findFirstVisibleNode(findModule.FIND_DEFAULT, frame);
assert.equals(utils.contentDocument.documentElement, node);
item = frame.document.getElementById('p3');
node = findModule.findFirstVisibleNode(findModule.FIND_BACK, frame);
assert.equals(item, node);
テスト用のドキュメントを新しいタブで開き、findFirstVisibleNode()
メソッドの返り値が期待通りかどうかを検証します。1つ目の検証は前方検索、2つ目は後方検索です。
同様にして、スクロールが発生する場合のテストも作成します。
yield utils.addTab(baseURL+'../res/longPage.html', { selected : true });
frame = utils.contentWindow;
node = findModule.findFirstVisibleNode(findModule.FIND_DEFAULT, frame);
assert.equals(utils.contentDocument.documentElement, node);
item = frame.document.getElementById('p10');
frame.scrollTo(0, item.offsetTop);
node = findModule.findFirstVisibleNode(findModule.FIND_DEFAULT, frame);
assert.equals(item, node);
frame.scrollTo(0, item.offsetTop - frame.innerHeight + item.offsetHeight);
node = findModule.findFirstVisibleNode(findModule.FIND_BACK, frame);
assert.equals(item, node);
item = frame.document.getElementById('p21');
frame.scrollTo(0, item.offsetTop - frame.innerHeight + item.offsetHeight);
node = findModule.findFirstVisibleNode(findModule.FIND_BACK, frame);
assert.equals(item, node);
スクロールされていない時、ページ途中までスクロールされている時、ページの最後までスクロールされている時の各ケースで前方検索と後方検索を行い、結果を検証します。
ここで、かなりの部分のコードが重複していることに気がついたでしょうか。このような場合、それぞれの検証の前で重複しているコードと検証とをひとまとめにして実行する関数(カスタムアサーション)を定義しておくと、テスト項目の追加が簡単になります。以下は、カスタムアサーションを使ってここまでのテスト内容を書き直した物です。
function testFindFirstVisibleNode()
{
var win = utils.getTestWindow();
win.resizeTo(500, 500);
assert.compare(200, '<', utils.contentWindow.innerHeight);
function assertScrollAndFind(aIdOrNode, aFindFlag)
{
var frame = utils.contentWindow;
var item = typeof aIdOrNode == 'string' ? frame.document.getElementById(aIdOrNode) : aIdOrNode ;
frame.scrollTo(
0,
(aFindFlag & findModule.FIND_BACK ?
item.offsetTop - frame.innerHeight + item.offsetHeight :
item.offsetTop
)
);
var node = findModule.findFirstVisibleNode(aFindFlag, frame);
assert.equals(item, node);
}
yield utils.addTab(baseURL+'../res/shortPage.html', { selected : true });
assertScrollAndFind(utils.contentDocument.documentElement, findModule.FIND_DEFAULT);
assertScrollAndFind('p3', findModule.FIND_BACK);
yield utils.addTab(baseURL+'../res/longPage.html', { selected : true });
assertScrollAndFind(utils.contentDocument.documentElement, findModule.FIND_DEFAULT);
assertScrollAndFind('p10', findModule.FIND_DEFAULT);
assertScrollAndFind('p10', findModule.FIND_BACK);
assertScrollAndFind('p21', findModule.FIND_BACK);
}
テストの実行
テストケースが完成したら、テストを実行してみましょう。実装の修正前なので、当然、このテストは「失敗」という結果が出ます。ですが、この段階では問題ありません。これから、このテストの結果が「成功」になることを目指して実装を修正していきます。
実装の修正
実装の修正内容については省略します。良いアイディアを思いついたら、それを実装に反映して、再度テストを実行してみましょう。テストに成功しないようであれば、まだ修正が必要です。
何度テストを実行しても結果が「成功」になるようになれば、実装の修正はひとまず完了です。修正内容をリポジトリにコミットするなり、修正済みの新しいバージョンとしてリリースするなりしましょう。
新たな問題が発覚した時や、仕様が変わった時は
以上で、今回発見された問題の修正は完了しました。
しかし、上記のテストだけではテストしきれないような、より複雑な条件でだけ発生するバグが新たに見つかるかもしれません。そのような場合は、テストを新たに追加して、それらがすべて「成功」するようになるまで修正してやりましょう。その時はもちろん、他のテストも同時に実行することを忘れないようにしましょう。
また、開発を進める中で、他の部分に加えた変更の影響を受けて上記のテストが失敗するようになることがあるかもしれません。そのような場合、再びテストが通るようになるように実装を修正する必要があります。
実装の仕様を変更した時にも、ここで作成したテストケースは「成功」しなくなる場合があります。このような場合は「ゴール」自体が変わったということになりますので、実装ではなくテストケースの側を修正しなくてはなりません。
まとめ
自動テストを使った開発では、メンテナンスする必要があるコードが「実装」と「テストケース」の2つになるため、一見すると、手間だけが倍増するように思えるかもしれません。
しかし、一連のテスト手順を自動化しておくことで、人の手によるテストでは見落としてしまいかねない思わぬ後退バグの発生に迅速に気づけるようになります。後退バグの発生に日々頭を悩ませている人は、是非、自動テストを開発に取り入れてみてください。
-
もちろん、後退バグの発生自体は未然には防ぎきれません。しかし、後退バグの発生にすぐ気がつくことができれば、コミットやリリースの前にその後退バグを修正できるため、他の共同開発者やユーザには後退バグの影響を与えずに済むようになります。 ↩