Hudsonをテストする

mixiでHudsonのユーザーの方に質問を頂いたので、Hudsonをどうやってリリース前にテストしているのかについてです。


まずは懺悔から。比較的最近まで、単体でテスト可能なクラスをテストするコードが若干ある他はHudsonには自動化されたテストはほとんどありませんでした。Web UIのテストは大変だとか、リリースサイクルの都合上仮にバグが見つかってもすぐに修正出来るとか、この手法で過去200リリース以上致命的な問題が発生したことはほとんど無かったから結果オーライとか、まぁ理由を弁解すれば色々有るのですが、やはり自動化テストがないのは良くないことです。Hudsonでは、長い間自動化テストに代わって人柱テストをやってきました。私が自分で社内向けに展開しているHudsonのインストレーションは非常に大規模なので、リリース前のバージョンをここへインストールし、一日位様子を見て大きな問題が見つからなければ出荷、という方法です。


この手法では、大きな問題が起こると社内でのHudsonの利用に問題がでます。社内でHudsonの重要性がましてクリティカルな用途に使われるようになるにつれ、これではだんだん都合が悪くなってきました。また、頻繁に実行されない機能や自分で使っていない機能にリグレッションが起こると、人柱テストは何の役にも立ちません。七月には、これが原因で、非常に重大なバグが2回ほどリリースに紛れ込むという事が起こりました。


そこで、遅ればせながら最近になって、自動化テストに力を入れるようになりました。


テストの有り難味はよくわかっているつもりです。以前関わっていたJAXB RIでは1500以上のテストケースを用意していました。SunのT2000という32コアのサーバでこれをがっつり並列実行して、全テストを30分以内に完了することができるようになっていました。これだけのテストケースが揃うと、テストでリグレッションを発見する事を前提にリスキーな変更でもどんどん加えていけますから、機能の追加やバグの修正が簡単にできます。JAXB RIは2.0になってから10回位のリリースをしてきたと思いますが、このためもあって、記憶に有る範囲では意図せざるリグレッションがリリースに紛れ込んだことはないと思います。


大規模なテストケースを効率的に運用できるようにするため、また、Hudsonのプラグインのテストを容易にするためにも、まずテストハーネスを整備することにしました。ただ、JUnitを使ってテストをガムシャラに書いていくのではなくて、Hudsonをテスト環境内で起動する方法を考え、テストでよく使われるであろう道具立てを整備し、できるだけテストコードを簡略に書けるように工夫をするという事です。


この方針で、今のところHudsonでは次のような道具立てを揃えました。

  • テストはHudsonもテストコードも含めて全て1つのJVMで動作します。これは、デバッグを容易にするという利点と、テスト関連の出力を全てまとめてJUnitのレポートに記録できるという利点があります。この手のWebアプリケーションのテストではついアプリケーションはいつものように別VM上で動かしてしまいがちですが、これは災難の元です。
  • テストコードは実行中のHudsonのオブジェクトにHTTP経由でなく直接アクセスすることが出来ます。Staplerの性質上Hudsonのコードは素直な木構造になっているのと併せ、これによって、テストの前段になるデータをセットアップする過程や、意図した動作が起こったかどうかを検証するのがより容易になります。また、書けるテストの幅も広がります。前項とも関係が有りますが、Webアプリケーションが別VMで動いていてアクセス手段がHTTPだけだと、検証はHTMLスクレイピングに全てを頼ることになり、悲惨です。
  • もちろん、テスト対象のHudsonにHTTP経由でアクセスして意図したHTMLが出力されているか、あるいはフォームの処理がうまくいっているかを検証する事もできるようにしました。この用途には幾らかパッチをあてたHtmlUnitを使っていて、また、先の直接アクセスの機能と組み合わせて、テストがこの2つを適宜切り替えて使うのを容易になるようにしています。
  • HtmlUnitJavaScriptもエミュレーションしてくれます。最初は本物のブラウザとの互換性に不安があったのですが、今のところ使っている感じでは、Seleniumは使わなくてもよさそうな位良い感じです。HtmlUnitの努力には脱帽。
  • Java agentを書いて、テスト実行中は全てのfinalクラス/finalメソッドからfinalを取り除くようにしました。これによって、テスト中はあらゆるクラスのあらゆるメソッドがモック可能になります。出荷されるコードにはちゃんとfinalが含まれているので、意図せざるクラスをサブクラス化されたくない...というfinalの本来の利点を殺さず、一方テスタビリティも犠牲になりません。
  • ノーテーションを用いて、テスト環境のセットアップの一部を宣言的に行えるようにしました。例えば、このテストはセキュリティが有効化されたHudsonの一部をテストする、というような場合に、0からスタートしてプログラム的にテスト対象環境を設定するのではなくて、アノーテーションを追加しておくと、ハーネスの方でそれをやってくれる、という仕組みです。
  • JAXBでの反省を生かして、テストケースと、そのテストケースを書く元になったバグリポートや電子メールを関連付ける為のアノーテーションを追加しました。

まだテストケース自体はそんなに書けていないのですが、滑り出しは順調だと思います。今後の課題としては...

  • 特定のOSでしか実行出来ないテストがあるので、JUnitのモデルを壊さずに、テストの実行を複数のマシンに広がってその結果を集計したい。
  • LDAPサーバだのある特定の状態に設定されたSubversionサーバだの、簡単にはキャプチャできない外部環境に依存するテストをどうするか。
  • テストの並列化。Hudsonはシングルトンに依存するところがあるので、複数のJVMを起動してテストを並列に実行するのがよさそう。
  • プラグインのテストがきちんとちゃんとできるようになっているかテスト。

より詳細に関しては英語ですがウェブサイトをどうぞ。