PFIインターンへ行ってきた(前編)〜結合テストの自動化環境を整えてきた〜

2011年3月1日〜3月31日の間、PFIインターンに行ってきた。本エントリは前編、後編にわけて書いてみようと思う。前編の今回はインターンの内容、インターンで開発した内容や設計、設計に至るプロセスやら私の成果について説明し、後編ではインターンのきっかけやインターンを実行する段階での私の心境、心構え、行動思想的な部分、環境適応やら震災云々について書こうと思う。

インターンの内容、その対象について説明し、どういう部分に悩みながら開発していったか紹介する(長いけど!)。最後に最終的な成果であるテストツールについて紹介し、社内でのインターン最終発表スライドで本エントリである前編を締める。

インターン内容

ミッション(テーマ):PFIの製品である統合型検索エンジンSedueの結合テスト(機能テスト)を自動化すること

テーマの決定については、PFIで需要のありそうなQA(品質保証)とテスト関連という分野と、私が提供できそうなテストツール開発経験がうまくマッチした感じ。Sedueの詳細は省くが、以下の製品紹介ページを読めばなんとなくシステム構成が把握できると思う。

製品概要 - 株式会社プリファードインフラストラクチャー
http://preferred.jp/sedue.html

Sedueはいわゆる分散システムである*1。書籍『Googleを支える技術』や、Googleの論文「Web Search for a Planet: The Google Cluster Architecture」などで検索エンジンを構成するサーバの役割を知っておくとわかりやすいと思う。特定の役割を担当するサーバがあり、それを複数組み合わせて検索エンジンというユーザから見て1つのシステムを提供する。

Googleを支える技術 ?巨大システムの内側の世界 (WEB+DB PRESSプラスシリーズ)

Googleを支える技術 ?巨大システムの内側の世界 (WEB+DB PRESSプラスシリーズ)

今回インターンでのテストとは、検索エンジンが正常に動作しているか機能テスト&結合テストだった(機能テストとは、インプットに対して、どんなアウトプットをするか、テストケースを記述してテストを行う。逆に、非機能テストでは例として、パフォーマンス、セキュリティなどがある)。


分散システムというのは、テストしたり、機能が正しく実装されていることを確認するのが難しい。手動・自動でやるにしても、テストを実行する環境と各プログラムの状態を整えるために、デプロイメントやプロセスの起動終了などなど、幾つかステップを踏む必要がある。

また、プログラム間で非同期(かつ順序が変わったり、メッセージが到達する保証が無かったりすると更に頭がこんがらがる)メッセージをやりとりしたり、キャッシュ整合性が...などと言い出すと更に複雑になる。まあ、今回のインターンでは、分散アルゴリズムではなく、検索エンジンを対象にテストを行うため、いかにしてデプロイメント・各プログラムの起動終了(プロセス制御)や、テスト記述を簡単に実現するかが課題だった。

テストに対する要求と、その解釈

インターンを開始して、結合テスト(機能テスト)を対象とすることや、どんな機能を必要としているか抽象的なゴールを聴いてから、徐々に具体的なゴールとして煮詰めていった。確認していた内容というのは、おおまかに以下のような内容。

  • 要望
    • 結合テストを自動化したい(ゴールは明確、実装手段はあいまい)
    • 結合テストには代表的なシナリオがあって、それを自動化できるようにしておきたい
    • プロセス起動や、検索対象となる文書登録をして...云々の完了"待ち"をスクリプトで簡素に記述したい(単体では簡単ではあるが、テスト記述の簡易さを求めたときどう設計するかが難しい、という感じ。"待つ"と言っても、複数異なる意味合いでタスクなり色々あるのです)
    • Hudson/Jenkinsでテスト実行数・エラー数がグラフで可視化できると嬉しい(テストツールへの要望)
  • 対象(範囲の限定)
    • 結合テストは機能テストだけを対象とする(非機能テストは対象外)*2
  • インターンでやったこと
    • テストしづらいコードをテストしやすい形(自動化できる状態)へ修正
    • 以上の要望のもと、私の認識できる部分から具体的な形を説明(提案)できる形にしつつ、ゴールから逸れていない確認してもらいつつ、テストツール・ライブラリなどを作り上げる

開発中(提案、設計、実装)は、「欲しいと言われた機能」に気を取られ過ぎずに「言葉では表現されていないけれど、最終的に欲している」形が実現できるように注意していた。何故そう思ったか? 「言ったことを鵜呑みにするな」という意味の話しをよく聴く(読む)し、『デザイン思考の道具箱』とか読んで、フィールドワーク的なことを意識して行動していたのかもしれない。といっても、具体的に何を提案したかはあまり記憶にない。

デザイン思考の道具箱―イノベーションを生む会社のつくり方

デザイン思考の道具箱―イノベーションを生む会社のつくり方


インターンの最中、特に以下のことで頭を悩ませた。

テストのコスト = 記述+実行+保守(運用):書きやすく、実行時間も短く(高頻度で実行できると嬉しい)、メンテナンス性に優れて移植性も高い(誰でも自動テストができる)というのが理想。理想と現実のギャップを可能な限り近づけるのがエンジニアとしての定めか?!

スクリプトで"タスク待ち"を記述すること自体は難しくない。ただし、テスト環境を整えることと、テストは密結合なので、同じ抽象度で記述したいと考えた。テストを記述するとき、抽象度の異なるスクリプト(例としてデプロイ云々・待ちに関するタスクはbashスクリプトで記述し、テストコードはRuby)になるのは避けたかった。どこのレイヤで何を分離、結合させるべきか。設定ファイル、分散しているプログラムの処理(何かタスクを実行して、その終了待ちがある)、テスト本体のコード。これらの抽象度・密結合・疎結合を一定に保ちつつ、テストを記述したい。抽象度というのは、ドメイン(領域)と言ってしまってもいいのかもしれない。

このあたりは、私が所属する研究室で開発しているScalaで書かれたKumoi*3というミドルウェアに影響を受けて、何が便利で、何を注意すべきかを活用した気がする。何かをオブジェクトへマッピングして利用したり、ラップするとユーザ側からは抽象化されてて使い安いことがある。逆に、フレームワークの層が多重に重なってしまい、内部で何が実行されているのかプログラマから想像できないことにも繋がって、実行時間の増加なども。Design by Contractを明示させる必要もある。ブロッキング、ノンブロッキングなところや、エラーハンドリングは統一しておきたい。

他にも以下の示すような、仕様、ツールの最終形態などなど、悩みつつ結論(妥協点)を模索していった。

  • テスト実行環境のサーバ構成をどう記述するか:プログラムとサーバのマッピングとかとか...。→ これはSedueの設定と密結合なので、テスト記述を助けるライブラリ側のレイヤで扱う方針で対処。デプロイ・プロセス制御は、SSHコマンドを呼び出すラッパーを作って対処。
  • テストシナリオはどう記述するか:グラフ(有向グラフ)でタスクジョブの依存云々〜とか考えた。けれど、有向グラフにした依存関係をテキストで記述するのは大変なので、シンプルに記述(定義)した順でテストを実行するという結論に。テストを記述するプログラマ(テスター)が責任を持って、依存関係のチェックをコードを記述する。
  • テストレポートの見やすさ、バグ修正のプロセスの想定:テスト実行後の、テスト(バグ)レポートと、それを踏まえた修正しやすさ、修正のプロセスも考える。テストでバグを見つけ、その後にバグの箇所・影響範囲などを特定してから修正することになる。レポートの結果は見やすい方が良い。また、結合テストという枠にとどまらず、回帰テストリグレッションテスト)などにも今後繋がってくる可能性が十分にある。
  • テストに要する実行時間が長さ:システムのテストは、複数プログラムが連携しているので、各種プロセス制御程度の、テストを準備する部分でも実行時間が取られがち。結合テストも実行時間長いだろうけど、複数のサーバ・プロセスが協調しているのでなおさら。
  • テストで発見したバグの再現性:テスト失敗時に、バグを再現するのに依存するデータがあるかもしれないので、テストクリーンアップ時に削除しないほうがいいかも。結局これは、テストを記述するプログラマの責任において実行する方針となった(と記憶している)。今回のテストシナリオでは対象としなかったが、RPCのタイミングに依存して発生するバグがあったり、それをテストすることもありそう。

成果:テストツール + ライブラリ

最終的な成果としては、dtestというテストツールと、テストを記述するのを便利にするためのライブラリを残すことができた。後者については、実はプロトタイピング(Proof of concept)なノリでテスト自動化の新天地を開拓するための汚いコードを量産していた(最終的なライブラリは、全部書き直してもらって綺麗なコードとして生まれ変わったので悔いは無い)。


dtestは、RubyRSpec風にテストのブロックを記述し、Test::Unitと似たようなassertion(assert_???メソッドを呼び出す)を記述する形となる。基本的には単体テストツールを使うのと変わりはない。以下のような機能がある。

  • assertion:単体テストのassertion(assert_equalメソッドとか)
  • expectation:失敗してもabortしないassertion。Google Testのexpect_??が便利だったので移植した感じ。
  • テストで記述したassertionでabortしたとき、abort(テストを中断)する範囲を指定できる
  • Hudson/Jenkins用のXML出力対応(JUnit風のXML
  • value-parameterized test:Google Testから移植*4

これらの機能は"欲しかったから"という理由ではあるが、少し以下に説明しよう。

  • 分散システムのテストでは、テスト実行するのに前提としている条件(複数プログラムそれぞれ、例えば起動している状態であるとか)がいくつもあるため、前提条件が崩れていたら即座にテストを中断したい(asesrtionでabort範囲の指定が可能である理由)。
  • 結合テストで、かつ分散システムなのでテストに要する時間は長くなる。テスト自体は失敗していても、テストの前提条件が崩れていない(テストを継続可能)なら残りのテストを実行させたいときもある。時は金なり。expect_??メソッドを使う。


ちなみに、 dtestのdはdistributed systemのD。dtest自体は、リモートマシンのコマンドを実行するような分散システム向けの機能は提供していない。Sedue Helper側でSSH経由でリモートコマンドを実行する機能を実装している。

結論:dtestはGoogle TestやRuby単体テストツールなどに影響を受けているが、分散システム(といってもSedueに特化してる)のテストで要求される機能を詰め込んだツールとなっている。オープンソースとして公開するのは、あわよくばdtestを使ってテストを便利にしたい人がいると嬉しいな、とか、外部へ公開できる私の成果を残しておけるので嬉しいですね、という感じ。といっても、テスト記述にあたってはSedue Helperや、最終発表スライドでも少し出てくるSedue Shellというコンポーネントが果たす役割が大きいため、結合テストをdtestで実現するには、対象とする分散システムを扱う抽象化ライブラリを適切に設計していく必要がある。それについても私がノウハウを文書化できるなら、ブログにでも書いていきたいところ。

最終発表スライド

flashでスライドが最後まで見ることができない場合は、slideshareからPDFをダウンロードすることもできます。

インターンはとても楽しかったです! ありがとうございました!
インターンをさせてもらう以上は、利益になるよう成果を出したかったのですが、テスト環境構築に貢献できたかと思っています。どういった経緯でインターンをお願いしたのかとか云々の詳細は(後編)へ続く。

*1:Wikipediaなどを見つつ、、まあ定義についてはタネンバウム本の『分散システム 原理とパラダイム(翻訳)』の定義より引用して「単一のコヒーレントシステムとして見える独立したコンピュータの集合」を想定。

*2:機能テストというのは、プログラムに対するinputに対してoutputの"ふるまい"を確認するテスト。非機能テストは、実行速度とか、なんとかbilityに類する可用性(長期間運用可能であるか)やパフォーマンス、ソフトウェアの安全性などが対象となってくる

*3:http://code.google.com/p/kumoi/

*4:value-parameterized testについて詳しい> http://d.hatena.ne.jp/nobu-q/20110103