テストの実行
TestApi によるフォールト挿入テスト
James McCaffrey
フォールト挿入テストとは、テスト対象のアプリケーションに意図的にエラーを挿入し、アプリケーションを実行して、アプリケーションがそのエラーに適切に対処するかどうかを判断するというプロセスです。フォールト挿入テストには、さまざまな形式があります。今月のコラムでは、TestApi ライブラリというコンポーネントを使用して、.NET アプリケーションの実行時にフォールトを挿入する方法について説明します。
このコラムの目的をご理解いただくには、図 1 のスクリーンショットをご覧いただくのが一番です。このスクリーンショットは、TwoCardPokerGame.exe というダミーの .NET WinForm アプリケーションで、フォールト挿入テストを実行しているところを示しています。コマンド シェルでは、FaultHarness.exe という C# プログラムが実行されています。このプログラムがテスト対象のアプリケーションの正常な動作に変更を加えるため、ユーザーが [Evaluate] というボタンを 3 回クリックすると、アプリケーションは例外をスローします。この状況では、Two Card Poker (ツー カード ポーカー) アプリケーションは、アプリケーションの例外が適切に処理しないため、システム生成のメッセージ ボックスが表示されています。
図 1 動作中のフォールト挿入テスト
この動作に関連する詳細を検討するため、このシナリオについて詳しく見てみましょう。コマンド シェルから FaultHarness.exe を起動すると、ハーネスでは自動的に、TwoCardPokerGame.exe の正常なコードの実行をインターセプトするコードをプロファイルする準備を行います。これを、フォールト挿入セッションと呼びます。
フォールト挿入セッションは DLL を使用して、アプリケーションの button2_Click メソッドの呼び出しの監視を開始します。このメソッドは、[Evaluate] ボタンのイベント ハンドラーです。フォールト挿入セッションは、ユーザーが [Evaluate] ボタンをクリックすると、最初の 2 回はアプリケーションがコードどおりに動作し、3 回目のクリックでは、アプリケーションが System.ApplicationException 型の例外をスローするように構成されています。
フォールト挿入セッションは、セッションのアクティビティを記録し、一連のファイルをテストのホスト コンピューターにログ記録します。図 1 では、アプリケーションの [Evaluate] をクリックすると、最初の 2 回は正常に機能しますが、3 回目のクリックで例外を生成しているのがわかります。
ここからは、テスト対象のダミーの Two Card Poker ゲーム アプリケーションについて簡単に説明し、図 1 に示した FaultHarness.exe プログラムのコードについて詳しく解説します。また、フォールト挿入テストを使用するのに適した状況と、別の技法の方が適している場合について、いくつかヒントを示します。FaultHarness.exe プログラム自体は非常に単純で、難しい作業の大半が TestApi DLL によって自動的に行われますが、ここで紹介するコードを理解し、独自のテスト シナリオに合わせて変更を加えるには、.NET プログラミング環境について十分に理解している必要があります。ただし、今回のコラムの説明を理解するのは、.NET の初心者であったとしても、それほど苦労することはなく、フォールト挿入テストが非常に興味深いもので、お使いのツールセットで使用すると役に立つ可能性があることをおわかりいただけると思います。
テスト対象のアプリケーション
テスト対象のダミー アプリケーションは、単純かつ典型的な C# WinForm アプリケーションで、Two Card Poker という架空のカード ゲームのシミュレーションを行います。アプリケーションは 2 つの主要コンポーネントから構成されます。1 つは UI を提供する TwoCardPokerGame.exe、もう 1 つは基になる機能を提供する TwoCardPokerLib.dll です。
ゲーム用の DLL を作成するには、Visual Studio 2008 を起動して、[ファイル] メニューの [新規作成]をポイントし、[プロジェクト] をクリックして、[新しいプロジェクト] ダイアログ ボックスで C# クラス ライブラリのテンプレートを選択します。ライブラリには、「TwoCardPokerLib」と名前を付けます。ライブラリの構造全体を図 2 に示します。TwoCardPokerLib のコードは非常に長いので、コラム内ですべてを示すことはできません。TwoCardPokerLib ライブラリの完全なソース コードと、FaultHarness フォールト挿入ハーネスについては、コラムに付属するコード ダウンロードをご覧ください。
図 2 TwoCardPokerLib ライブラリ
using System;
namespace TwoCardPokerLib {
// -------------------------------------------------
public class Card {
private string rank;
private string suit;
public Card() {
this.rank = "A"; // A, 2, 3, . . ,9, T, J, Q, K
this.suit = "c"; // c, d, h, s
}
public Card(string c) { . . . }
public Card(int c) { . . . }
public override string ToString(){ . . . }
public string Rank { . . . }
public string Suit { . . . }
public static bool Beats(Card c1, Card c2) { . . . }
public static bool Ties(Card c1, Card c2) { . . . }
} // class Card
// -------------------------------------------------
public class Deck {
private Card[] cards;
private int top;
private Random random = null;
public Deck() {
this.cards = new Card[52];
for (int i = 0; i < 52; ++i)
this.cards[i] = new Card(i);
this.top = 0;
random = new Random(0);
}
public void Shuffle(){ . . . }
public int Count(){ . . . }
public override string ToString(){ . . . }
public Card[] Deal(int n) { . . . }
} // Deck
// -------------------------------------------------
public class Hand {
private Card card1; // high card
private Card card2; // low card
public Hand(){ . . . }
public Hand(Card c1, Card c2) { . . . }
public Hand(string s1, string s2) { . . . }
public override string ToString(){ . . . }
private bool IsPair() { . . . }
private bool IsFlush() { . . . }
private bool IsStraight() { . . . }
private bool IsStraightFlush(){ . . . }
private bool Beats(Hand h) { . . . }
private bool Ties(Hand h) { . . . }
public int Compare(Hand h) { . . . }
public enum HandType { . . . }
} // class Hand
} // ns TwoCardPokerLib
アプリケーション UI のコード
基になる TwoCardPokerLib ライブラリ コードを作成したら、ダミーの UI コンポーネントを作成します。Visual Studio 2008 で、C# WinForm アプリケーションのテンプレートを使用して新しいプロジェクトを作成し、アプリケーションに「TwoCardPokerGame」と名前を付けます。
Visual Studio デザイナーを使用して、[ツールボックス] コレクションからアプリケーションのデザイン サーフェイスに、Label コントロールをドラッグして、コントロールの [Text] プロパティを "label1" から "Two Card Poker" に変更します。続いて、Label コントロールをさらに 2 つ ([Text] プロパティを "Your Hand" と "Computer's Hand" に変更します)、TextBox コントロール を 2 つ、Button コントロールを 2 つ ([Text] プロパティを "Deal" と "Evaluate" に変更します)、ListBox コントロールを 1 つ追加します。8 つのコントロールのうち、いくつかのコントロールは、textBox1、textBox2、listBox1 など、既定の名前を変更しません。
デザインにコントロールを適切に配置したら、button1 コントロールをダブルクリックして、Visual Studio でボタンのイベント ハンドラーのスケルトンを生成し、コード エディターに Form1.cs ファイルを読み込みます。この時点で、ソリューション エクスプローラ内の TwoCardPokerGame プロジェクトを右クリックし、コンテキスト メニューから [参照の追加] をクリックして、TwoCardPokerLib.dll ファイルを指定します。Form1.cs には、using ステートメントが追加されているため、ライブラリのクラス名を完全修飾する必要はありません。
次に、アプリケーションに、クラス スコープの静的オブジェクトを 4 つ追加します。
namespace TwoCardPokerGame {
public partial class Form1 : Form {
static Deck deck;
static Hand h1;
static Hand h2;
static int dealNumber;
...
オブジェクト h1 はユーザーの手札で、オブジェクト h2 はコンピューターの手札です。続いて、Form コンストラクターに初期化コードを追加します。
public Form1() {
InitializeComponent();
deck = new Deck();
deck.Shuffle();
dealNumber = 0;
}
Deck コンストラクターでは、クラブのエースからスペードのキングまで順番に、52 枚のトランプの組を 1 組作成します。そして、Shuffle メソッドで組のカードの順番をランダムに並べ替えます。
次に、button1_Click メソッドにコード ロジックを追加します (図 3 参照)。2 枚の手札をそれぞれ選択するために、deck.Deal メソッドを呼び出して、deck オブジェクトから 2 枚のカードを取り出します。続いて、取り出した 2 枚のカードを Hand コンストラクターに渡して、手札の値を TextBox コントロールに表示します。button1_Click メソッドでは、ListBox コントロールにメッセージを表示することによって例外を処理するのがわかります。
図 3 カードの配布
private void button1_Click(
object sender, EventArgs e) {
try {
++dealNumber;
listBox1.Items.Add("Deal # " + dealNumber);
Card[] firstPairOfCards = deck.Deal(2);
h1 = new Hand(firstPairOfCards[0], firstPairOfCards[1]);
textBox1.Text = h1.ToString();
Card[] secondPairOfCards = deck.Deal(2);
h2 = new Hand(secondPairOfCards[0], secondPairOfCards[1]);
textBox2.Text = h2.ToString();
listBox1.Items.Add(textBox1.Text + " : " + textBox2.Text);
}
catch (Exception ex) {
listBox1.Items.Add(ex.Message);
}
}
次に、Visual Studio デザイナー ウィンドウで、button2 コントロールをダブルクリックして、コントロールのイベント ハンドラーのスケルトンを自動生成します。簡単なコードを追加して、2 つの Hand オブジェクトを比較し、ListBox コントロールにメッセージを表示します。button2_Click メソッドでは、直接例外を処理していないのがわかります。
private void button2_Click(
object sender, EventArgs e) {
int compResult = h1.Compare(h2);
if (compResult == -1)
listBox1.Items.Add(" You lose");
else if (compResult == +1)
listBox1.Items.Add(" You win");
else if (compResult == 0)
listBox1.Items.Add(" You tie");
listBox1.Items.Add("-------------------------");
}
フォールト挿入ハーネス
図 1 に示したフォールト挿入ハーネスを作成する前に、テストのホスト コンピューターに主要な DLL をダウンロードします。これらの DLL は、.NET ライブラリのコレクションの一部で、TestApi という名前が付いています。TestApi については、testapi.codeplex.com (英語) を参照してください。
TestApi ライブラリは、ソフトウェア テスト関連のユーティリティのコレクションです。TestApi ライブラリには、一連のマネージ コードのフォールト挿入 API が用意されています (この API については、blogs.msdn.com/b/ivo_manolov/archive/2009/11/25/9928447.aspx (英語) を参照してください)。フォールト挿入 API の最新のリリース (ここではバージョン 0.4 です) をダウンロードして解凍します。ダウンロードの内容と、フォールト挿入のバイナリを配置する場所について簡単に説明します。
バージョン 0.4 では、.NET Framework 3.5 を使用して作成されたアプリケーションのフォールト挿入テストをサポートします。TestApi ライブラリは現在も開発が続いているため、CodePlex サイトで、このコラムで紹介する手法の最新情報を確認してください。また、フォールト挿入用の TestApi ライブラリの主力開発者である、Bill Liu のブログ (blogs.msdn.com/b/billliu/、英語) で、最新情報とヒントを確認することをお勧めします。
フォールト挿入ハーネスを作成するには、Visual Studio 2008 で新しいプロジェクトを作成する際、C# コンソール アプリケーションのテンプレートを選択します。アプリケーションに「FaultHarness」と名前を付けて、プログラムのテンプレートに最小限のコードを追加します (図 4 参照)。
図 4 FaultHarness
using System;
namespace FaultHarness {
class Program {
static void Main(string[] args) {
try {
Console.WriteLine("\nBegin TestApi Fault Injection environmnent session\n");
// create fault session, launch application
Console.WriteLine("\nEnd TestApi Fault Injection environment session");
}
catch (Exception ex) {
Console.WriteLine("Fatal: " + ex.Message);
}
}
} // class Program
} // ns
F5 キーを押して、ハーネスのスケルトンをビルドして実行し、FaultHarness ルート フォルダーに \bin\Debug フォルダーを作成します。
TestApi ダウンロードには 2 つの重要なコンポーネントが含まれています。1 つは TestApiCore.dll で、解凍したダウンロードの Binaries フォルダーにあります。この DLL を、FaultHarness アプリケーションのルート ディレクトリにコピーします。続いて、ソリューション エクスプローラで FaultHarness プロジェクトを右クリックし、[参照の追加] をクリックして、TestApiCore.dll を指定します。次に、Microsoft.Test.FaultInjection の using ステートメントを、フォールト挿入ハーネスのコードの先頭に追加して、ハーネスのコードから TestApiCore.dll の機能に直接アクセスできるようにします。また、詳しくは後で説明しますが、System.Diagnostics 名前空間の Process クラスおよび ProcessStartInfo クラスにアクセスするため、System.Diagnostics の using ステートメントも追加します。
フォールト挿入のダウンロードに含まれるもう 1 つの重要なコンポーネントは、FaultInjectionEngine という名前のフォルダーです。このフォルダーには、32 ビットおよび 64 ビット バージョンの FaultInjectionEngine.dll が含まれています。FaultInjectionEngine フォルダー全体を、FaultHarness 実行可能ファイルを含むフォルダー (この場合は C:\FaultInjection\FaultHarness\bin\Debug\ です) にコピーします。ここで使用したフォールト挿入システムのバージョン 0.4 では、FaultInjectionEngine フォルダーを、ハーネスの実行可能ファイルと同じ場所に配置する必要があります。また、このシステムでは、テスト対象のアプリケーションのバイナリを、ハーネスの実行可能ファイルと同じフォルダーに配置することも必要です。そこで、TwoCardPokerGame.exe ファイルと TwoCardPokerLib.dll ファイルを、C:\FaultInjection\FaultHarness\bin\Debug\ にコピーします。
まとめると、TestApi フォールト挿入システムを使用する際に適切な手法は、ハーネスのスケルトンを作成して実行し、ハーネスの \bin\Debug ディレクトリを作成して、ハーネスのルート ディレクトリに TestApiCore.dll ファイルを配置し、\bin\Debug に FaultInjectionEngine フォルダーとテスト対象のアプリケーションのバイナリ (.exe および .dll) を配置します。
TestApi フォールト挿入システムを使用するには、テスト対象のアプリケーション、フォールトをトリガーするテスト対象のアプリケーションのメソッド、フォールトをトリガーする条件、およびトリガーするフォールトの種類を指定する必要があります。
string appUnderTest = "TwoCardPokerGame.exe";
string method =
"TwoCardPokerGame.Form1.button2_Click(object, System.EventArgs)";
ICondition condition =
BuiltInConditions.TriggerEveryOnNthCall(3);
IFault fault =
BuiltInFaults.ThrowExceptionFault(
new ApplicationException(
"Application exception thrown by Fault Harness!"));
FaultRule rule = new FaultRule(method, condition, fault);
システムでは、テスト対象のアプリケーションと、ハーネスの実行可能ファイルは同じフォルダーに存在する必要があるため、テスト対象のアプリケーションの実行可能ファイルの名前には場所のパスは必要ありません。
TestApi によるフォールト挿入に慣れていないと、挿入したフォールトをトリガーするメソッドの名前を指定する際によく問題が発生します。メソッド名は、"<名前空間>.<クラス>.<メソッド>(<引数>)" の完全修飾形式で指定しなければなりません。ildasm.exe ツールを使用して、テスト対象のアプリケーションを検証し、トリガー用のメソッドのシグネチャを確認するという手法をお勧めします。Visual Studio ツールの特別なコマンド シェルから ildasm.exe を起動して、テスト対象のアプリケーションを指定して、ターゲット メソッドをダブルクリックします。図 5 は、ildasm.exe を使用して、button2_Click メソッドのシグネチャを確認する例を示しています。
図 5 ildasm.exe を使用してメソッド シグネチャを確認する
トリガー メソッドのシグネチャを指定する際は、メソッドの戻り値の型も、パラメーター名も使用しません。適切なメソッド シグネチャに正しく修正するには、少し手間をかける必要があったり、エラーが発生したりすることがあります。たとえば、button2_Click ターゲット メソッドでは、最初、次のシグネチャを使用しようと考えていました。
TwoCardPokerGame.Form1.button2_Click(object,EventArgs)
しかし、これは次のように修正する必要がありました。
TwoCardPokerGame.Form1.button2_Click(object,System.EventArgs)
TestApi のダウンロードには Documentation フォルダーがあり、そこには、コンストラクター、ジェネリック メソッド、プロパティ、オーバーロードされた演算子など、さまざまな種類のメソッド シグネチャの適切な構成方法について、優れたガイダンスを提供する概念ドキュメントが含まれています。ここでは、テスト対象のアプリケーションに存在するメソッド対象にしていますが、次のコードに示すように、基になる TwoCardPokerLib.dll 内のメソッドを対象にすることもできます。
string method = "TwoCardPokerLib.Deck.Deal(int)"
トリガー メソッドを指定したら、次は、フォールトをテスト対象のアプリケーションに挿入する際の基準となる条件を指定します。今回の例では TriggerEveryOnNthCall(3) を使用しました。おわかりのように、この条件では、トリガー メソッドを 3 回呼び出すたびにフォールトが挿入されます。TestApi のフォールト挿入システムには、TriggerIfCalledBy(<メソッド>)、TriggerOnEveryCall など、優れた一連のトリガー条件が用意されています。
トリガー条件を指定したら、次は、テスト対象のシステムに挿入するフォールトの種類を指定します。ここでは BuiltInFaults.ThrowExceptionFault を使用しています。TestApi のフォールト挿入システムには、例外が発生するフォールトだけでなく、組み込みの戻り値の型を設定できるフォールトも用意されており、不適切な戻り値の型を実行時にテスト対象のアプリケーションに挿入できます。たとえば、値 -1 (おそらく不適切な値でしょう) を返すように、トリガー メソッドを設定できます。
IFault f = BuiltInFaults.ReturnValueFault(-1)
フォールトのトリガー メソッド、条件、およびフォールトの種類を指定したら、次は、新しく FaultRule を作成して、その規則を新たな FaultSession に渡します。
FaultRule rule = new FaultRule(method, condition, fault);
Console.WriteLine(
"Application under test = " + appUnderTest);
Console.WriteLine(
"Method to trigger injected runtime fault = " + method);
Console.WriteLine(
"Condition which will trigger fault = On 3rd call");
Console.WriteLine(
"Fault which will be triggered = ApplicationException");
FaultSession session = new FaultSession(rule);
準備段階のすべての処理が完了したら、フォールト挿入ハーネスのコードを作成する最後の手順として、フォールト挿入セッションの環境内でテスト対象のアプリケーションをプログラムから起動します。
ProcessStartInfo psi =
session.GetProcessStartInfo(appUnderTest);
Console.WriteLine(
"\nProgrammatically launching application under test");
Process p = Process.Start(psi);
p.WaitForExit();
p.Close();
フォールト挿入ハーネスを実行すると、フォールト挿入セッション内でテスト対象のアプリケーションが起動します。このとき、FaultInjectionEngine.dll は、トリガー条件が true の場合、トリガー メソッドが呼び出される状況を監視します。ここでは、テストを手動で実行していますが、フォールト挿入セッションでテストを自動的に実行することもできます。
フォールト挿入セッションの実行中、セッションに関する情報は現在のディレクトリ、つまり、フォールト挿入ハーネスの実行可能ファイルとテスト対象のアプリケーションの実行可能ファイルが存在するディレクトリのログに記録されます。これらのログ ファイルを検証すると、フォールト挿入ハーネスの開発中に発生する可能性がある問題を解決するのに役立ちます。
ディスカッション
今回のコラムで紹介した例と説明は、テスト対象のアプリケーション用にフォールト挿入ハーネスを作成するのに大いに役立ちます。ソフトウェア開発プロセスの一部のアクティビティと同様に、リソースが限られているため、フォールト挿入テストを実行する場合のコストとメリットを分析してください。アプリケーションによっては、フォールト挿入テストを作成する労力を費やす価値がない場合もありますが、多くのテスト シナリオではフォールト挿入テストが非常に重要なります。医療機器や飛行システムを制御するソフトウェアを想像してください。このようなシステムでは、アプリケーションは絶対的に堅牢で、あらゆる予期しないエラーに適切に対処できなければなりません。
フォールト挿入テストには、ある意味、皮肉が込められています。それは、なんらかの例外が発生することが予想されても、ほとんどの場合、理論的にはその例外が発生しないようにプログラムを設計でき、そのように設計されたプログラムが適切に動作するかどうかをテストすることができるという皮肉です。しかし、このような状況でも、フォールト挿入テストは、作成するのが難しい例外を生成するのに役立ちます。また、System.OutOfMemoryException など、予想するのが非常に難しいフォールトを挿入することもできます。
フォールト挿入テストは、変形テストとも関連しているため、場合によっては変形テストと混同されることがあります。変形テストでもテスト対象のシステムにエラーを意図的に挿入しますが、問題のあるシステムに対して、既存のテスト スイートを実行して、テスト スイートで、新しく作成されたエラーがキャッチされるかどうかを確認します。変形テストを使用することにより、テスト スイートの効果を測定して、最終的に、テスト ケースの対象範囲を増やすことができます。今回のコラムで説明したように、フォールト挿入テストの主な目的は、テスト対象のシステムがエラーを適切に処理するかどうかを確認することです。
Dr. James McCaffrey は Volt Information Sciences Inc. に勤務し、ワシントン州レドモンドにあるマイクロソフト本社で働くソフトウェア エンジニアの技術トレーニングを管理しています。これまでに、Internet Explorer、MSN サーチなどの複数のマイクロソフト製品にも携わってきました。また、『.NET Test Automation Recipes: A Problem-Solution Approach』(Apress、2006 年) の著者でもあります。連絡先は jammc@microsoft.com (英語のみ) です。
この記事のレビューに協力してくれた技術スタッフの Bill Liu と Paul Newson に心より感謝いたします。