The Rational Edge
オープンソース時代のテスト手法、そのノウハウ
――Part2:テストのロードマップ
by Len DiMaggio
Software Quality
Engineer
Rational Software
IBM Software Group
2003/6/17
■テストのロードマップ
これから始めようとするテストは次のようなタイプに分けることができる。これらのテストタイプを説明する順番は、実施する順番とは必ずしも一致しないことを覚えておいていただきたい。「スモークテスト」(初期テスト)は最初に実施しなくてはならないが、ほかのタイプのテストについては、テストされるソフトウェアにとって妥当な順番であればよい。例えば「スループットテスト」などは、パフォーマンスの改善を目指してコードを変更した場合に検証するための時間が残るよう、テストサイクルの早い段階で実施した方がよい場合が多い。
■テストのタイプ
「Part1:どこから始めるか」の「何をどのように、どこから始めるか」の説明にあるように、テストはさまざまなタイプのものを実施することになる。
-
スモークテスト
インストールとコンフィグレーションテスト
データの互換性テスト
プログラム間の連携テスト
モニタリングとロギングテスト
セキュリティテスト
パフォーマンスとスループットテスト
信頼性テスト
メンテナンス性と拡張性テスト
以下、これらのタイプを詳細に説明し、併せてテストの計画と実施に役立つ「教訓」も紹介する。
●スモークテスト
統合しようとしているソフトウェアを十分調査し、そのデザイン、依存性、そしてインターフェイスを理解しているとしよう。だが、これで本格的にテストを始められると思ったら大間違いだ。
あなたは、自分のビジネスパートナーやオープンソースソフトウェアの身元を完全に信頼しているのかもしれないが、「信頼は獲得するもの」という格言は現代にも生きている。最初のテストではソフトウェアの動作を検証する。分解できない変数だらけの複雑な方程式を解こうというのがソフトウェア統合テストだ。自社で開発したソフトウェアについてはこれまでの流れを大体把握しているかもしれないが、「外部の」ソフトウェアについてはまったく知識がない場合もあり得る。
最初に実施するテストは「スモークテスト」だ。これは元来「ハードウェアコンポーネント」テストと呼ばれていたもので、エンジニアが慎重に部品に電源を投入し、これから実施する大規模テストに向けて十分それが安定していることを確認するものだ。もし、部品から煙が出たり発火したりするようなことがあれば、この最初のテストは失敗ということになる。一方、ソフトウェアのスモークテストは、基本テストをいくつか実行し、そのソフトウェアが大規模テストに耐えられるかどうか確認する。
西暦2000年問題で騒然となる前のことだが、私はスモークテストが実を結んだケースに遭遇したことがある。当時、私は大企業に勤めており、その会社は規模の小さいある企業とインターネットセキュリティ製品やサービスに関する提携を進めていた。この提携企業は規模は小さいものの、とにかく印象深いところだった。技術スキルが高く、プロの仕事をしていた。同社のソフトウェアはいつもうまく機能し、マニュアルも非の打ちどころのないものだった。ソフトウェアの社内テスト計画も見せてくれたが、これらのドキュメントも見事な出来栄えだった。同社のY2Kテスト計画など特に感動的で、ソフトウェアエンジニアリングの教科書としてお手本に使えるのではと思えた。われわれは最初のスモークテストを行わずに同社のソフトウェアを導入することも検討したが、最終的にはいくつかのテストを実施することにした。われわれは、システムの日付を「1999年12月31日午後11時59分0秒」に変えて再起動してみた。システムは問題もなくすぐに再起動し、その動作を何度も何度も繰り返した。まさにY2Kのバグがあったのだ。われわれがこのバグを報告すると、彼らは仰天して24時間以内に修正用のパッチを送付してきた。これ以外はソフトウェアに特に大きなバグは見つからなかったが、それ以降、アップデートが送られてくると必ずスモークテストを実施するようになった。
●インストールとコンフィグレーションテスト
統合される各サブシステムが独立して正常に機能することが確認できたら、今度はこれらをまとめる作業に取り掛かる。この統合システムのインストレーションやコンフィグレーションの検証では、どのような問題を予想すべきだろうか。主なものを次に挙げる。
-
環境変数
システム資源の利用
同一ソフトの複数バージョン
ポートの争奪戦
以下で各項目について説明する。
<<環境変数>>
アプリケーションのホームもしくは作業ディレクトリをポイントする環境変数を設定する、あるいは実行イメージ、ライブラリ、classファイルへのディレクトリパスの正しい順番を確立しておくと非常に重宝する。これらの変数を設定することで、プログラムと依存ソフトウェアが容易に見つかり、明示的な(そしておそらく非常に長い)パス名を使って参照する必要がなくなる。しかし、PC上のレジストリ設定を含むこれらの変数は、複数のプログラムが同じ設定を定義しているとうまく動作しない。このような場合は、プログラムをインストールするたびに以前インストールされたプログラムの設定が上書きされてしまう。ところで、パスの定義については文法が正しいといってパスの動作も正しいと思わないこと。私も以前、パスの定義の組み合わせに関連する問題に遭遇したことがある。パスがあまりにも長くなってしまい(255文字に達してしまった)、JSPコンパイルエラーが出てきたのだ。コンパイラがテンポラリのjavaやclassファイルを作成できず、「Server error 500 cannot write file」(サーバエラー500 - ファイルに書き込めません)のメッセージから過度に長いパス定義に思い当たるまでにはだいぶ考えてしまった。
教訓:ソフトウェアをインストールしたことに起因するシステム構成の変化は正確かつ詳細に把握しておくに越したことはない。
<<システム資源の利用>>
1つのプログラムがほかのプログラムのパフォーマンスに悪影響を与えるほどシステムのリソース(メモリやCPUなど)を大量に消費することは珍しいことではない。仲間のソフトがリソースに困るようプログラムをうっかり設定してしまっては泣きっ面にハチだ。私も少し前にこのような例を目にしたことがある。データベースにアクセスするサーブレットの基本機能テストを行っていたとき、このデータベースプロセスのサイズがどんどん大きくなってメモリを消費していることに気付いたが、このデータベースは有名な市販のプログラムをそのまま使っていたので変に思えた。われわれがメモリ不足だと思ったのは、実は「知識不足」だったのだ。われわれはデータベースのメモリ割り当てを、設定容量まで、もしくは最大容量まで、のいずれかに指定できたことを見落としていたのだ。
教訓:デフォルトの値をよく考えずにそのまま使ってはならない。設定可能なものはすべて見極め、理解したい。
<<同一ソフトの複数バージョン>>
オープンソース運動のマイナス面の1つが、自分が独自に開発した製品をほかのベンダの複数の製品と統合する場合、同じサポートソフトウェアを何度もインストールしなくてはならなくなる場合がある点だ。私はJava Runtime Environment(JRE)でこれを経験した。われわれは、自前のソフトウェアにこれを添付したが、データベースベンダもこれを添付し、アプリケーションサーバベンダもこれを添付していた。これを複数インストールしたことでディスク容量が無駄になったものの、これら3つすべてが同一バージョンだったことで少なくとも深刻な問題には至らなかった。また、同じベンダのWebサーバに複数の製品がパッケージングされている例も見たことがある。どの製品にもApache Webサーバが同梱されていたため、これらも深刻な事態には陥らなかった。もし同じ製品の複数のバージョンが存在する場合は、すべてが同じポート(例:HTTPはポート80番)を使おうとするので、デフォルトの設定値をそのまま使ってはならない。
教訓:統合するプログラムの中に組み込まれたソフトウェアだけを考えていてはならない。同梱されているサポートソフトウェアにも必ず注意することだ。また、JREが怪しいと思ったら、インストールされたすべての拡張機能(XMLパーシングや暗号など)のチェックも怠らないこと。
<<ポートの争奪戦>>
先に述べたように、ソフトウェアが独自にWebサーバをパッケージングしている場合がある。このような場合は、これらすべてがHTTPトラフィックにポート80を使う可能性が高いため、デフォルトのコンフィグレーションを使うと問題に遭遇する可能性がある。しかし、これらの問題を解消したからといって安全だと思ってはいけない。これより大きい番号が振られ、問題発生をまったく予想していないポートでもコンフリクト(衝突)が発生する可能性はある。私は先日、アプリケーションサーバとウィンドウマネージャのパッケージがデフォルトのポート設定に同じ9000のポート番号を要求したという状況を経験している。ヒント:どのポートが使用中か調べるには、UNIXシステムでは/etc/servicesファイル、PCではWINNT/system32/drivers/etc/servicesファイルから見ていくとよいだろう。現在アクティブなプログラムがどのポートを使っているのか知りたい場合は、UNIXでもPCシステムでも「netstat」コマンドを試してみるとよい。
教訓:インストール時にエラーが出なかったといってシステム全体が大丈夫だと思ってはいけない。複数のプログラムが同じポートにアクセスしようとすれば衝突が発生する可能性があるのだ。
●データの互換性テスト
統合システムで永続性のあるデータを扱うときは大半の場合はデータベースを使うことになり、複数のサーバ上で複数のデータベースを運用する可能性が高い。この場合、統合を進めるシステムの境界をデータが越えるときの扱い方によっては問題が発生するかもしれない。
注意すべき具体的な問題点は次に示すとおり。
-
データベーススキーマの互換性
「不良データ」の取り扱い
<<データベーススキーマの互換性>>
「これの何が大きな問題なのだろう?」「データにもSQLにも互換性があるではないか?」といった考えた方は間違っている。扱っているさまざまなデータベーススキーマのインプリメンテーションには大きな違いが潜んでいる。
SQLの文法の違い:システムのインストール手順にはデータベース、テーブル、そしてレコードの作成が含まれているのではないだろうか。これらを処理する最も簡単な方法は、SQL文を実行するスクリプトを利用することだ。どのシステムのどのデータベースでも動くスクリプトを書くことが理想だが、残念ながらSQLのシンタックスには違いが存在する。例えば、予約語はデータベース間で異なるため、これの使用をチェックしなくてはならなくなる。しばらく前のことだが、「OID」という名前のフィールドがあったためにSQLサーバデータベースからテーブルを作成し直せなかった例を見たことがある。これはSQL Serverでは違うがOracleでは予約語になっている。お確かめあれ。
データタイプの違い:データフィールドでもデータベースの非互換性がしばしば見られる。ここでもOracleとSQL Serverの例を引き合いに出すと、Oracleは日付を各7バイトの固定長フィールド(世紀、年、月、日、時、分、秒にそれぞれ対応)で内部のDATEフォーマットに保存するが、SQL ServerはこれをDATETIMEタイプ(固定長数値)で保存する。これは問題を引き起こすのだろうか? それはデータの書き出し、読み込み、そして変換をどのように行うかによる。
教訓:スキーマと、それが示すデータを単純にデータベースからデータベースへ移行できるとは思わないこと。移行やマージを実施する前に、時間をかけて各データベースを調査することだ。
<<「不良データ」の取り扱い>>
「動かない理由によい理由はない」という格言は私のお気に入りだ。しかし、「悪い」理由はいくつかある。1つはシステムユーザーインターフェイスでデータのリレーションを強制しているかどうかの検証といったエラー処理の欠如だ。私は数年前に忘れられない経験をしている。私の部署では上司から命令を受け、リレーショナルデータベースにアクセスする各種データベースアプリケーション(GUIのフロントエンドもJava、C、およびPerlなどを使った多岐にわたるもの)のテストを行うことになった。問題は、これらのアプリケーションが社内利用のために開発され、カスタマに販売するものではなかったため、ユーザーインターフェイスが扱いにくく、エラー処理のコードも不十分な点だった。さらに、これらのアプリケーションを旧型システムと入れ替えるという意向もからみ、データフォーマットが新しいアプリケーションに受け入れてもらえないことで障害が頻繁に発生した。(契約した)プログラマは欠陥のあるコードを書いていることは分かっているが、上層部の命令に従っているだけだと訴えた。上層部だって? 「バグではない。データが悪い」というのが彼らのお決まりの言葉だ。私たちに彼らの態度を完全に変えさせることはできなかったが、(社内の)ユーザーグループと協力して信頼性要件を定義することにより、コードの大半を何とか改良することができた。もちろん、ユーザーの観点からすれば「不良データ」が原因であっても、プログラムの障害は障害に変わりない。
教訓:問題のないデータに障害は起こらず、自分のシステムがその対応に追われることはない、などと仮定しないこと。
●プログラム間の連携テスト
いままでに、新しいハードウェアやソフトウェアをインストールしようとして、「初めにお読みください」と書かれたインストレーション関連のドキュメントがいくつもあった経験はお持ちでないだろうか? 同じような問題は、システムやプログラムを統合するときにも起こり得る。Windowsシステムでは、プログラム間の依存性をレジストリの中にセットしておくことが簡単にできる。こうすればシステムをリブートしてもプログラムが指定した順番どおりに確実に再起動する。UNIXシステムでは、システムのスタートアップスクリプトを使うことで同じような設定ができる。
統合テストでは、統合されるサブシステムのスタートアップ時の要求事項がコンフリクトを起こしていないか検証する必要がある。例えば、もしサブシステムA、B、そしてCの統合を進めていて、AはBの前、CもBの前に起動しなくてはならないような場合は、AとCにも依存性があるかどうか調べなくてはならない。syslogに書き込みをするUNIXのデーモンプログラムなどはその好例だ。この場合、syslogのデーモンは必ず最初に起動しなくてはならない。私が見てきた中には、デーモンがどうしても起動せず、これをシェルから手動で起動するとsyslogにエラーメッセージを書き出すだけという例があった。スタートアップスクリプトの中に書かれたコマンドのシンタックスが手入力の場合とまったく同じでも、システムをリブートしてこのデーモンを起動しようとしてもまったく駄目で、エラーメッセージさえ書き出してくれなかった。このデーモンがsyslogに何か書き出そうとしてもsyslogのデーモンが(まだこの時点では)起動していないことに気付いたのは相当な時間が経過してからだった。
教訓:すべてのサーバ、サービス、デーモン、プロセスなどが起動しているかどうかだけでなく、これらがいつどのように起動しているのかにも注意する必要がある。
●モニタリングとロギングテスト
統合テストで最も難しく、いら立つのは、1つあるいは複数のプログラム/サブシステムが原因で発生したシステム障害に遭遇し、この障害がどこで起こっているのかまったく分からないときだ。問題のシステムがパッと見では問題なく動いているようなのに、内部プロセスやデータフローのステータスをまったく見ることができないときも問題だ。複雑なシステムをテストする場合は、サブシステムの動作を堅牢なモニタリングやロギングサブシステムで検証する必要がある。
モニタリングとロギングがこれほど重要なのはなぜか? それは、これらを行わずしてシステムの動作を知ることは不可能だからだ。私が携わってきたデザインのレビューに関して最も鮮明に覚えている思い出の1つが、データベースサーバのデーモンによってサポートされるロギングのレベルだ。私がデザイン仕様を調べていくと、このデーモンが生成するロギング情報の種類やレベルについての記述がまったくなかった。そこで、このことを設計主任に尋ねると、「そのようなものは不要だ。なくてもサーバは動作する」という答えが返ってきたのだ。彼のコメントを聞いた私は、「好きなだけ食べながらやせられます」という前日の晩に見たテレビCMを思い出した。よくぞいったものだ。
そこで私は、サーバのデーモンがハングしたり、クラッシュしたときはどうするのか、そして障害の発生時期や発生前の状況をどのようにして知るつもりなのか尋ねた。すると彼の口からは、「そのようなことは問題ではない。サーバを再起動すればいい」という答えが返ってきた。私が突っ込むと彼は自己防衛に走り、さらに突っ込むと彼は自分の殻に閉じこもり、完全な防衛体制を敷いたのだった。私はテストの初期段階でサーバに負荷をかけて彼らの間違いを証明することにしたが、その機会は決して訪れなかった。数週間後には大規模なデモが行われてしまったのだ(ありがたいことに社内ユーザー向けだったが)。サーバは頻繁にハングしたため、昼休みまでに8回ほどリブートが行われ、何が問題なのかだれにも分からなかった。その後、ログ情報が生成されるようデザインは急きょ変更された。
検証、モニタリング、ロギングでは何に注意したらよいだろう? ロギングメッセージの精度を検証しなくてはならないのは明らかだが、そのほかに何があるだろう?
-
共通点
サブシステム障害時の処理
これらを詳しく見ていこう。
<<共通点>>
システムは、それぞれに大きく異なるロギングメカニズムを持つ複数のサブシステムによって構成されている場合が多い。(1)ソフトウェアテストエンジニアであるあなたがシステム全体のプログラムの動作やトランザクション(端から端まで)をトレースできるよう、そして(2)システムのどこにある問題でもソフトウェアサポートエンジニアがデバッグできるよう、これらの異なるメカニズムの「つじつまが合う」よう検証したい。
システム全体を通した1つのトランザクションはどうすればトレースできるだろうか? トランザクションに対する入力と、最後の出力だけを調査するのではないのだから、内部のデータ、ロジック、そしてプロセスのフローは深く理解すればするほど好ましい。また、(生成されたロギング情報を見て)中間の複数のステップで処理されるデータについても調べることになる。トランザクションをトレースしてシステムで考えられるすべての経路を検証したとの確認はできるのだろうか? おそらく無理だろう。スケジュール、予算、テスト資源の許す範囲でテスト範囲を最大限までカバーするようなユーザーのシナリオをいくつか作ることになる。
複数の複雑なサブシステムによって構成される複雑なシステムが生成するログを隅々まで調べるのは細かく面倒な作業になる。この作業は、表面上関係のない情報を論理的に結び付けることで完了できる。システムのログを通してこれらのトランザクションをトレースするとき、あなたは何を探すのだろうか。次のような共通点だ。
詳細情報のレベルと有用性:さまざまなログが一貫して詳細であればシステム全体の流れをトレースするのが容易になる。逆にいえば、遭遇した問題に関する具体的情報を含まないエラーメッセージはほとんど役に立たない。例えば、あるサブシステムでファンクションのリターンコードをトレーシングしていて「想定外の障害」といったログメッセージがあれば、トレースはそこで立ち往生してしまう。このような情報はまったく役立たないのだ。ログは常に詳細に採取するよう設定し、受け取る情報は最も簡単なものでも実用的なレベルにしておきたい。
データフォーマット:古い人間なのかもしれないが、私は(ASCII形式の)テキストファイルが好きだ。これならばさまざまなプログラムで編集できるし、「grep」などのユーティリティも使え、テキスト中にあるパターンを検索するために簡単なPerlスクリプトを作成することも、単に読むことや、電子メールで送信することもできる。独自のプログラムでしか読めない独自のバイナリフォーマットのログをいくつも前にして作業するのは苦痛でしかない。理想は、自分のシステムが生成するさまざまなログファイルを読むことのできる1つの統合ユーティリティを用意することだ。このようなものがない場合は、少なくともすべてのログを同じアプローチで読んだり検索したりするための手段を用意しておくことだ(たとえ、自由に使えるツールが「cat
thelog.txt | more」だけしかなくてもである)。
<<サブシステム障害時の処理>>
テストされるシステムは、自分の障害から復帰する能力を持った複数のサブシステムによって構成されている場合が多いが、これらが1つの「システム」として連携する場合は取りこぼしも出てくる。しばらく前のこと、あるサードパーティベンダが提供するデータベースと別のベンダが提供するアプリケーションサーバの組み合わせで問題に遭遇したことがある。このアプリケーションサーバは、初期化時点でデータベースが起動処理を終了して完全に稼働しているとの前提で動作するのだ。アプリケーションサービスの前にデータベースサービス(そのときの環境はWin-32プラットフォームだった)を起動してしまえばいいのだから問題ないと思われるかもしれない。ところがそうはいかない。後で分かったことだが、このデータベースサービスは複数のプロセスによって構成されており、プロセスによって起動に要する時間がまちまちだったのだ。その結果、システムを再起動するとアプリケーションサーバの一部のプロセスが必ず障害を起こしてしまった。われわれが困ったのは、そのデータベースベンダは問題の存在を一切否定し、アプリケーションサーバベンダの方はデータベースが原因だと主張したことだ。このデータベースとアプリケーションサーバの組み合わせを搭載したシステムにとっては、再起動してから障害を起こしたプロセスを手作業で処理することが回避策だった。
教訓:問題が起こるかどうか思案するのに時間を浪費しないことだ。問題は起こるし、そうなった場合に備え、デバッグと解決に必要な情報を用意しておかなければならない。もしロギング機能のデザインに関してもっと勉強したいと思ったら、まずBrian Marickの「Using Ring Buffer Logging to Help Find Bugs」(バグ発見に向けた循環配列ログ手法の利用)注という白書を読まれるのがよいだろう。
(続く)
(編集局より)今回は「オープンソース時代のテスト手法、そのノウハウ――Part2:テストのロードマップ」の「モニタリングとロギング」まで紹介した。次回は続きとして、「セキュリティテスト」「パフォーマンスとスループットテスト」「信頼性テスト」「メンテナンス性と拡張性テスト」まで一気に紹介する。
【Notes】
Available at http://visibleworkings.com/trace/Documentation/ring-buffer.pdf
本記事は「The Rational Edge」に掲載された「Testing at the boundaries between integrated software systems Part II: A roadmap for testing」をアットマーク・アイティが翻訳したものです。 |
IT Architect 連載記事一覧 |