【ITニュース解説】What a Browser Extension's Test Suite Cannot Reach
2026年09月08日に「Dev.to」が公開したITニュース「What a Browser Extension's Test Suite Cannot Reach」について初心者にもわかりやすく解説しています。
ITニュース概要
ブラウザ拡張機能の単体テストでは、ブラウザAPIとの境界や内部モジュール連携のバグを見落とすことがある。画像印刷でハングアップする問題は、`display:none`要素と`img.decode()`の予期せぬ相互作用で発生した。実際のDOM環境に近い「ハーネス」でテストし、根本的な修正と、境界部分のテストの重要性を学んだ。単体テストだけでは不十分で、実践的なテストが必要だ。
ITニュース解説
ITシステムを開発する上で、プログラムが正しく動くことを確認するための「テスト」は非常に重要である。しかし、テストがすべて成功して「問題なし」と判断されても、実際の動作で予期せぬ不具合が発生することがある。この記事では、Firefoxのスクリーンショット拡張機能「Longshot」の開発経験から、そのようなテストの落とし穴と、より効果的なテスト方法について解説する。
開発者は「Longshot」という拡張機能を作成し、初期段階では130個のテストがすべて成功し、一つもエラーがない状態だった。これは一見するとプログラムが完璧に動作しているように見える。しかし、いざ拡張機能の「印刷」機能を使おうとすると、印刷ダイアログが全く表示されず、プログラムが無限に停止してしまうという重大な問題が発生した。通常のテストではこの問題を一切検出できなかった。テストは「合格」と報告され続け、問題があることを示唆する兆候は何もなかったのである。
このバグの根本原因は、開発者のコードとブラウザの動作が複雑に絡み合う「境界」にあった。印刷機能では、画像を小さなスライスに分割し、それぞれを一時的なファイルのような形式(blob URL)に変換し、それを読み込むための処理(img.decode())を待っていた。しかし、印刷時にはブラウザが適用する特定のスタイルシートによって、画像を格納する領域が画面上から「非表示」(display:none)に設定されていた。この「非表示」の状態では、img.decode()という画像のデコード処理が完了せず、永遠に次の処理へ進まなかったのである。開発者は、このdecode()の挙動、スタイルシートがDOM(Webページの構造)に与える影響、そしてそれらの処理順序という、自分のコードとブラウザの様々な要素がぶつかる地点でバグに遭遇した。通常の単体テストは、純粋な関数(外部に依存しない計算など)の動作を検証するものであり、ブラウザのDOMや非同期処理の複雑な相互作用をシミュレートすることはできなかったため、このバグを見逃した。
同じ時期に発見された別のバグも、同様の「境界」で発生した。拡張機能内で「PDFを開く」という選択肢を選ぶと、エディタで開く機能が動作しなかったのだ。これは、PDFデータをエディタ機能に渡す際に、エディタ側が画像としてPDFを処理しようとしたため、正しくデコードできなかったのが原因である。このケースでは、自分のコード内部の異なる処理ステップ間でのデータの受け渡しという「境界」で問題が発生した。各処理ステップ自体は個別にテストされていたが、それらが連携する「接合部分」はテストされていなかったのである。
これらの経験から、開発者はプロジェクトで発生したバグのほとんどが、自分のコードとブラウザAPIの境界、あるいは自分のコード内部の異なるモジュール間の境界で発生するというパターンに気づいた。純粋な関数の単体テストだけでは、このような境界での複雑な相互作用を網羅的にテストすることは不可能であった。問題は「テストのカバレッジ(網羅率)が低い」のではなく、「テストの対象とする形状そのものが、実際に発生する問題を捉えきれていなかった」点にあった。
最初の印刷機能のバグに対する解決策は、根本的な原因を取り除くものだった。画像をblob URLにエンコードしてdecode()を待つのではなく、canvas要素という、ブラウザが直接描画できる要素をコードから追加するように変更したのだ。canvasは同期的に描画されるため、画像を読み込むための非同期処理や、decode()を待つ必要がなくなった。これにより、display:noneの状態がdecode()処理に影響を与えるという問題そのものが解消された。これは単なる一時しのぎの回避策ではなく、バグが発生する可能性のある「失敗のクラスそのもの」を設計から排除するアプローチであった。もし一時的な回避策を選んでいたら、ブラウザの特定バージョンや特定の条件下でのみ正しい動作をするという不安定な状態が続き、将来的に再び問題となる可能性を残していただろう。
この経験から、開発者は新しいテスト手法として「ハーネス」と呼ばれるテストページを導入した。これはtest-pages/harness.htmlというWebページで、実際の拡張機能のオプション画面や印刷処理パイプラインを、ブラウザのAPIをシミュレートした環境(スタブ化されたAPI)で実行する。これにより、最初の印刷機能の動作停止という重大なバグを、このハーネスの初回実行で発見することができた。さらに重要なのは、このハーネス自体が信頼できることを確認するステップだ。開発者は、意図的にプログラムを停止させるような「ハング」状態をテストハーネスに注入し、それが正しく「タイムアウト」として報告されることを確認した。テストが「失敗する可能性がある」ことを実際に示すことで、そのテストが本当に問題を発見できる能力があることを証明したのだ。もしこのステップがなければ、テストハーネスもまた、何も検出しない「グリーン」な状態が続くばかりで、信頼できないものになっていた可能性があった。
OCR(光学文字認識)機能のテストハーネスも、境界テストの強力な例である。これは、既知のテキストをレンダリングし、OCRでそれを認識させ、認識結果が元のテキストと一致するかを検証する。さらに、認識された単語の座標が、画面上の描画位置と一致するか、そして最終的に生成されたPDFファイル内で、不可視のテキストレイヤーが正しく含まれているか、などが検証される。これらのテストは、単に開発者が書いた関数が正しい値を返すかだけでなく、その結果が下流の処理(PDF生成、ユーザーインターフェースなど)にどのように渡され、最終的に期待通りの振る舞いをしているかを検証している。これは、まさに「境界」をまたいだ総合的なテストの典型である。
もちろん、まだテストが十分ではない部分も残っている。例えば、拡張機能の主要機能である「キャプチャエンジン」は、ブラウザとの連携が最も深く複雑な部分だが、現時点では純粋な関数のテストしかなく、今回の印刷バグと同じ種類の「境界の盲点」が残されていると開発者は正直に認めている。また、メール下書き機能や、実際にプリンターから何が出てくるかといった最終的な出力結果まではテストできていないことも明言されている。
この経験から得られる普遍的な教訓は、テストが全て成功して「グリーン」な状態になった後で本当に問うべき質問は、「コードの何パーセントがテストでカバーされたか」ではなく、「このテストは、私が最も恐れている種類のバグ(例えば、非同期処理がいつまでも完了しない、スタイルシートとAPIが予期せぬ相互作用を起こす、異なるモジュール間で不適切なデータが渡されるなど)を検出できるか」であるということだ。自分の書いたコードがどこで終わり、自分が制御できない外部の要素(ブラウザAPI、OS、他のライブラリなど)がどこから始まるのか、その「境界線」を明確にし、その境界線に対してどれだけのテストが存在するかを確認することが重要となる。そして、どのような種類のテストを書いたとしても、そのテストが本当に失敗を検出できることを証明するため、一度は意図的に失敗させてみるべきである。この意識を持つことで、より堅牢で信頼性の高いシステムを開発できるようになるだろう。