レベル: 中級 Andrew Glover (aglover@stelligent.com), President, Stelligent Incorporated
2006年 11月 29日 パフォーマンス・テストは通常、アプリケーション開発サイクルの最後まで棚上げにされます。パフォーマンス・テストが重要でないというわけではなく、不明な変数がありすぎるため効率的にテストするのが困難だからです。今月の「コード品質を追求する」シリーズでは、Andrew Glover が開発サイクルの一環としてのパフォーマンス・テストを提案し、2 つの簡単なテスト方法を紹介します。
アプリケーション・パフォーマンスの検証は、アプリケーション開発中にはいつも二の次にされています。ここで私が強調したいのは、アプリケーション・パフォーマンスの「検証」であることに注目してください。アプリケーションのパフォーマンスは常に第一の関心事ですが、検証となると、開発サイクルに組み入れられることはほとんどありません。
パフォーマンス・テストは通常、さまざまな理由で開発サイクルの後の方まで延期されます。私の経験からすると、企業がパフォーマンス・テストを開発プロセスに組み込まない理由は、開発途中のアプリケーションから何を期待するべきかわからないためです。数字は出されても、それは予期する負荷を基にした推測に過ぎません。
パフォーマンス・テストが最大の関心事になるのは一般的に、以下の 2 つのいずれかが起こった場合です。
- 稼働中に顕著なパフォーマンス上の問題が表面化した場合
- クライアントや有望なカスタマーが資金提供に同意する前に、パフォーマンス上の数字について尋ねた場合
今月の記事では、このような問題が突如持ち上がる前にパフォーマンスをテストするための 2 つの簡単な手法を紹介します。
JUnitPerf を使用したテスト
ソフトウェア開発の初期段階で、JUnit を使って基本的なパフォーマンスに関する概算値を出すのは簡単です。JUnitPerf フレームワークを使えばすぐに、テストを簡単な負荷テスト、さらには耐久テストにまで変えられます。
JUnitPerf では、TimedTests および LoadTests という 2 つのタイプのテストを作成できます。いずれもデコレーター設計パターンに基づき、JUnit の suite メカニズムを利用します。TimedTests ではテスト・ケースの上限を設け、この時間を上回った場合にテストは失敗となります。LoadTests はタイマーと連動し、構成したタイマーで区切られた時間を指定の回数、特定のテスト・ケースに人工的負荷をかけて実行します。
適切に時間制限を設けたテスト
JUnitPerf の TimedTests では、時間しきい値を関連付けてテストを作成できます。このしきい値を上回ると、(テスト・ロジック自体は実際には成功したとしても) テストは失敗と判断されます。時間制限を設けたテストは何よりも、ビジネスに不可欠なメソッドに関するパフォーマンス上の数字を解明し、モニターするのに役立ちます。テストをもう少し詳細にして、一連のメソッドをテストし、特定の時間しきい値に適合することを確実にすることもできます。
例えば、あるウィジェット・アプリケーションで、ビジネスに不可欠な特定のメソッド (例えば createWidget() というぴったりの名前のメソッドなど) が厳しいパフォーマンスしきい値のターゲットとなっているとします。この場合おそらく必要となるのは、create() メソッドを実行する際の機能面についてパフォーマンス・テストを行うことです。機能面のパフォーマンスについては、開発サイクルの後の方になってから、さまざまなチームがさまざまなツールを使用して究明するのが一般的ですが、通常これでは問題のメソッドを正確に特定できません。そこで、代わりに「早い段階から頻繁にテストする」という方法で行うことにしたとします。
TimedTest の作成は、通常の JUnit テストを作成するところから始まります。つまり、TestCase やその派生物を拡張し、test で始まるメソッドを作成します。リスト 1 を参照してください。
リスト 1. ウィジェットの簡単なテスト
public class WidgetDAOImplTest extends TestCase {
private WidgetDAO dao;
public void testCreate() throws Exception{
IWidget wdgt = new Widget();
wdgt.setWidgetId(1000);
wdgt.setPartNumber("12-34-BBD");
try{
this.dao.createWidget(wdgt);
}catch(CreateException e){
TestCase.fail("CreateException thrown creating a Widget");
}
}
protected void setUp() throws Exception {
ApplicationContext context =
new ClassPathXmlApplicationContext("spring-config.xml");
this.dao = (WidgetDAO) context.getBean("widgetDAO");
}
}
|
JUnitPerf はデコレーター・ベースのフレームワークなので、実際にこれを利用するには、suite() メソッドを指定して既存のテストを TimedTest で修飾しなければなりません。TimedTest は Test、そしてテストの実行時間に対する最大値をパラメーターとして使用します。
また、ブール値フラグを 3 番目の引数 (false) として渡すというオプションもあります。この場合、最大時間を超過すると即座に JUnitPerf が強制的に失敗と判断するので、テストが早く失敗となります。ブール値フラグを渡さない場合は、テスト・ケースが完了するまで実行されてから失敗することになります。その違いは微妙で、オプション・フラグを使用しないでテストを実行すると、失敗した場合でもテストの総所要時間がわかります。一方 false を渡すと、テストの実行が完了するまで待つ必要がありません。
例えば、リスト 2 では 2 秒の上限を設定して testCreate() を実行しています。実行時間の合計がこの上限時間を超えると、テスト・ケースは失敗となります。オプションのブール値パラメーターを渡していないため、テストは完了するまで実行されますが、これには時間がかかります。
リスト 2. suite メソッドの実装による TimedTest の作成
public static Test suite() {
long maxElapsedTime = 2000; //2 seconds
Test timedTest = new TimedTest(
new WidgetDAOImplTest("testCreate"), maxElapsedTime);
return timedTest;
}
|
このテストは、JUnit フレームワーク内で正常に実行されます。既存の Ant タスク、Eclipse ランナーなどは、このテストを他の JUnit テストと同じように実行します。唯一の違いは、このテストはタイマーとの関係で実行されるということです。
十分すぎるほどの負荷テスト
テスト・シナリオではメソッド (または一連のメソッド) で時間しきい値を検証する一方、JUnitPerf は負荷テストを簡単に行えるようにします。TimedTests のときと同様に、JUnitPerf の LoadTests は、JUnit Test を追加のスレッド化情報でラップして負荷をシミュレートすることで、デコレーターとして機能します。
LoadTest では、シミュレートする複数のユーザー (つまりスレッド) を指定することや、スレッドを起動するタイミング・メカニズムを提供することもできます。JUnitPerf には、ConstantTimer および RandomTimer という 2 つのタイプの Timer があります。これらのタイマーを LoadTest に指定すると、より実際的にユーザー負荷をシミュレートできます。Timer を使用しない場合は、すべてのスレッドが同時に起動されます。
リスト 310 人のユーザーをシミュレート対象とした負荷テストで、ConstantTimer を使用して実装されています。
リスト 3. 負荷テストを作成するために実装された suite メソッド
public static Test suite() {
int users = 10;
Timer timer = new ConstantTimer(100);
return new LoadTest(
new WidgetDAOImplTest("testCreate"),
users, timer);
}
|
testCreate() メソッドは 10 回実行され、それぞれのスレッドは 100 ミリ秒間隔で起動されることに注目してください。しきい値は設定されていません。これらのメソッドは単に完了するまで実行され、実行に失敗した場合は適宜、JUnit が失敗をレポートします。
スタイルで修飾する
デコレーターは、単一の修飾に限らてはいません。例えば、Java™ I/O では FileInputStream を BufferedReader を使用した InputStreamReader で修飾することも可能です (BufferedReader in は new BufferedReader(new InputStreamReader(new FileInputStream("infilename"), "UTF8")) と同じであることを覚えておいてください)。
修飾は複数のレベルで行われることがありますが、JUnitPerf の TimedTests および LoadTests の場合も同じです。この 2 つのクラスが互いを修飾すると、強力なテスト・シナリオになります。例えば、ビジネス・ケースに負荷をかけるとともに、時間しきい値も適用するなどといったシナリオです。あるいは、前述の 2 つのテスト・シナリオを以下のように単純に組み合わせることもできます。
- testCreate() メソッドに負荷をかける。
- すべてのスレッドが時間しきい値の時間内に完了するように指定する。
上記の設定を適用するために標準 Test を TimedTest で修飾された LoadTest で修飾すると、リスト 4 のようになります。
リスト 4. 修飾された負荷テストと時間制限付きテスト
public static Test suite() {
int users = 10;
Timer timer = new ConstantTimer(100);
long maxElapsedTime = 2000;
return new TimedTest(new LoadTest(
new WidgetDAOImplTest("testCreate"), users, timer),
maxElapsedTime);
}
|
上記のリストを見るとわかるように、testCreate() を 10 回実行し (スレッドを 100 ミリ秒間隔で起動)、各スレッドが 2 秒以内に完了しない場合は、テスト・シナリオ全体が失敗します。
使用する際の注意事項
JUnitPerf はパフォーマンス・テストのフレームワークですが、このフレームワークに関連して設定する数字はあくまでも概算であることを念頭に置いてください。JUnitPerf で修飾されたすべてのテストは、JUnit フレームワークを介して実行されるため、オーバーヘッドが追加されます。フィクスチャーを利用する場合、それはなおさらです。JUnit 自体はすべてのテスト・ケースを setUp と tearDown() メソッドで修飾することから、この実行時間をテスト・シナリオのコンテキスト全体で考慮する必要があります。
そのため、私は好みのフィクスチャーのロジックを利用してテストを作成することがよくありますが、同時に基準となる数字を判断するためにブランク・テスト・ケースも実行しています。こうして出した数字は概算ですが、テストのためのあらゆるしきい値に追加する基準値として役立ちます。
一例として、DbUnit を使用するフィクスチャーのロジックで修飾したブランク・テストを実行するのに 2.5 秒かかるとします。この場合、設定するすべての JUnitPerf テストしきい値には、この追加時間を考慮しなければなりません。これは、リスト 5 のようなベンチマーク・テストを見ると分かります。
リスト 5. JUnitPerf のベンチマーク・テスト
public class DBUnitSetUpBenchmarkTest extends DatabaseTestCase {
private WidgetDAO dao = null;
public void testNothing(){
//should be about 2.5 seconds
}
protected IDatabaseConnection getConnection() throws Exception {
Class driverClass = Class.forName("org.hsqldb.jdbcDriver");
Connection jdbcConnection =
DriverManager.getConnection(
"jdbc:hsqldb:hsql://127.0.0.1", "sa", ");
return new DatabaseConnection(jdbcConnection);
}
protected IDataSet getDataSet() throws Exception {
return new FlatXmlDataSet(new File("test/conf/seed.xml"));
}
protected void setUp() throws Exception {
super.setUp();
final ApplicationContext context =
new ClassPathXmlApplicationContext("spring-config.xml");
this.dao = (WidgetDAO) context.getBean("widgetDAO");
}
}
|
リスト 5 のテスト・ケース testNothing() は何も実行しないことにお気付きですか。setUp() メソッドを実行するための総所要時間を確定することが、唯一の目的だからです (もちろん、DbUnit によってデータベースもセットアップされますが)。
さらに、マシンの構成、そして JUnitPerf テストを実行している期間の特定の時点で何が実行中であるかによって、テスト時間が変わってくるという点にも注意してください。JUnitPerf テストを固有のカテゴリーに配置しておくと、通常のテストから区別するのに役立つことがよくあります。つまり、JUnitPerf テストは、CI 環境内でのコード・チェックインなどのように、テスト実行中に毎回実行するわけではないということです。さらに、最終的には、計画されたシナリオやパフォーマンス・テストが考慮される環境でだけ JUnitPerf テストを実行する特定の Ant タスクを作成することになります。
とにかく試してみてください
JUnitPerf によるパフォーマンス・テストは決して正確な科学ではなく、開発ライフ・サイクルの初期段階でアプリケーション・コードのおおよそのパフォーマンスを解明し、モニターするための優れた方法です。その上、これはデコレーター・ベースの JUnit 拡張フレームワークなので、JUnitPerf を使って簡単に既存の JUnit テストを修飾することができます。
負荷を受けた状態でアプリケーションが果してどのように実行するのかと始終心配して過ごすことを考えてみてください。JUnitPerf によるパフォーマンス・テストは、それよりも有意義なことに時間を割くための手段で、しかもアプリケーション・コードの品質を確実にしてくれます。
参考文献 学ぶために
製品や技術を入手するために
議論するために
著者について  | 
|  | Andrew Gloverは合衆国ワシントン特別区にある、Vanward TechnologiesのCTO(最高技術責任者)です。Vanward Technologiesは自動化テスト・フレームワークの構築を専門としており、ソフトウェアのバグ発生数や統合時間やテスト時間の減少、また全体的なコード安定性改善に貢献しています。
|
記事の評価
|