再インテグレーションテスト

2024/06/15

Webサービスの品質管理の主役として、 2年前にe2eテストを導入して引き続き機能している。
Ruby on Railsを作ったDHHが システムテストの限界を書いていて、フレーキーであるという指摘はその通りだ。
じっさいにテストしていて、テストツールのSeleniumやChromeブラウザの動作をテストしているような事象を見ることの方が多い。

DHHが指摘している問題とは別の限界があることに気づき、数日かけて別のテスト方式を追加実装していた。
Webサービスは複雑度が上がってきたため、e2eではカバーしきれない状況になった。

テストケースは起きうる状態を再現して挙動をチェックするソフトウェアであり、状態を網羅する度合いを上げると検出力が高まる。 たとえば2つのモジュールがそれぞれ自由度M, Nといった空間を持つとき、2つを組み合わせた状態空間は M × N に増える。 ところが、e2eテストは実際の画面操作をシミュレートする都合上、表層のMパターンを再現するのがせいぜいといった限界がある。 実行速度の遅さやフレーキネスもネックとなり、すべての空間を探索するのが事実上不可能だという結論に至った。

ライブラリの品質検査

e2eの不足感は、依存しているライブラリのトラブルが増えてきたことによる。
当然ながら自分で書いたコードをカバーするようにテストを書くことになるため、背後に隠れたライブラリの挙動テストは手薄になる。

従来の一般的な感覚では、ライブラリの挙動をテストしてもバグ検出の収穫は少ないという前提があった。
広く使われているソフトウェアほど問題が検出済になっている可能性が高いからだ。

最近目につく問題は、このような実装済の機能の不備ではなく、機能が実装されないことによる不備という新たなパターンが多い。
標準規格であってもその導入スピードは各実装ごとに異なり、相対的に乗り遅れているライブラリがバグの原因となっている。

経緯を確認してみると、要するに開発体力が落ちていてキャッチアップが万全と言えないプロジェクトが増えているように見える。

原因特定に難がある

e2eでは適切に動作していることは確認できるが、エラーが起きた際の原因特定に弱点がある。

比較してみて気づいたのだが、どうやらSeleniumではJavascriptのコールトレースを取得できない。
各種ログを取得する機能はあるものの、console.trace()が出力するメッセージにログレベルが割り当てられておらず、詳細なログ出力を指定してもSeleniumに流れてこない。 console.trace()が返す値はundefinedであるため、ログに埋め込む手段もとれない。

とくに深層に埋まった依存ライブラリに原因がある場合、トレースなしでエラーのメカニズムを特定することは難しく、手作業による状況分析に多大な時間をとられることになる。

denoの進化が大きい

Javascript言語はブラウザ上で動作するコードとサーバーで動作するコードの両方の用途をカバーしてはいるものの、長らく両者の互換性は低いままだった。

サーバー用途はここまで Node.jsがデファクトスタンダードであり続けた。そして、npmなどの開発ツールはサーバーだけでなくブラウザコードも含めてNode.js準拠のものばかりだったため、ブラウザ向けコードもコンパイルしなければ動作しない状況が続いてきた。

ECMAscriptとして標準仕様が改訂されていくなかで、いずれでもない新たな文法への統合が進んだ結果、ブラウザ向けの新規格コードが denoサーバーでほぼ支障なく動作するところまで来た。

今回インテグレーションテストを実際に書いてみるまで、「ブラウザコードがdeno上でそのまま動くのではないか」という憶測に半信半疑だった。これは過去何度もトライしてきたことで、エラーに直面して断念して来たからだ。

決定打は、npm参照の安定性が大きく向上したことだろう。
denoはもともとnpmを排除したかったのだが、ライブラリ群が移行すべき新たなレジストリが定まらなかった。deno向けの新興レジストリがベンチャー企業として活動していたのだが、いずれも使いものにならないまま消えていった。
近年denoはnpmを扱えるように方針転換し、ようやく挙動が安定してきた。

denoはChromeと同じV8ランタイムに加えて、ブラウザAPIも実装しており、全体としてブラウザ挙動の再現度が高い。
WebAssemblyやWeb Workersといった高度な拡張機能もコードを変更することなくブラウザと同様に動作している。

動作していることじたいが神秘的とさえ言える。
denoが話題になっていないのは、従来のツールセットが貧弱すぎて、とうに誰もJavascriptの高度な拡張に挑戦しなくなってしまったからなのではないか。

レンダリングの除外

e2eの難点に対処するために、テスト対象からレンダリング機能を除外したインテグレーションテストとして再構成した。
ブラウザのレンダリング機能はdomというモジュールが担っており、denoにもNode.jsにもライブラリは存在している。

ここに至るまで数年がかりの試行錯誤を重ねてきた所感を一言で言えば、要するにdomがテストに適していない

Webに限らず、UI機能はXMLのような属性セットのツリーを操作する形式で実装されており、各テストケースの状態を簡潔に記述できない。簡潔に記述できないということは、状態空間を網羅するうえでネックになる。
テスト用途の高レベル言語を追加する必要があるのだと思う。

また、レンダリングじたいがとてもフレーキーだ。1件のテストケースが不安定になると他のケースも同時にfailする挙動が多く、もはやコードの品質をテストしているとは言えない。

レンダリングを除外する方法は、ブラウザのdocumentオブジェクトを参照しているコードがテスト対象に含まれないように構成することだ。
依存しているライブラリが不用意にdocumentを参照しているケースがあり、その場合には間接的にもimportのチェーンに混入しないようにする必要がある。

新たに開発するアプリケーションであればテストケースと並行して開発することで、とくに苦労せず導入できるだろう。
既存のアプリケーションは、おそらくUIライブラリを分離する追加開発が必要になる。

e2eテストとインテグレーションテストの併用

UIコードを含まないインテグレーションテストをdeno上で実行すると、Seleniumを用いたe2eテストと比べて100倍程度高速に動作し、挙動も安定する。

e2eテストは100ケースを超えてくると、実行や保守に不安定さが出る。本体のコードを拡張していくにつれ、網羅性の不足も目立ってくるが、需給ギャップを埋める手段がない。
denoを用いたインテグレーションテストは、数千ケース程度のCIを動作させられるのではないかと思う。また、exportしている任意の関数やオブジェクトをテストできるため、依存しているライブラリの詳細挙動のチェックも可能だ。

Javascriptは登場からもう30年も経つが、ようやくまともなテストツールが整ってきたという印象だ。
ここまで置き換えが効かない言語になるとは誰も想像していなかった、ということだろう。

e2eテストは実環境の挙動を確認できるため、実装できるなら併用した方が良い。網羅性は期待できないが、サンプリング実装にも意義がある。
レンダリングをテストに含めるのであれば実ブラウザを用いるべきで、jsdomなどのエミュレーションは無意味だろう。

⁋ 2024/06/15↻ 2024/06/21