テストの実行

QICT によるペアワイズ テスト

James McCaffrey

サンプル コードのダウンロード

すべてのソフトウェア テスター、開発者、マネージャーにとっては、ペアワイズ テストの原理についてしっかりとした知識が不可欠です。今月号のコラムでは、ペアワイズ テストとは正確にはどのようなものかを説明し、製品レベルの高い品質を備えた QICT というペアワイズ テストの完全な C# ソース コードを提供します。簡単に言うと、ペアワイズ テストとは、管理できないほど膨大なテストケースの入力数を、テストを実行するシステムで最低限バグを明らかにできる、ごく少量の入力数に削減する手法です。ペアワイズ テストを説明する、つまり、今月のコラムでお伝えしようとしていることを明らかにするため、2 つのスクリーンショットを用意しました。まず、図 1 に示すような Windows フォームベースのダミー アプリケーションを考えてみましょう。このアプリケーションには 4 つの入力パラメーターがあります。最初のパラメーターはテキストボックス コントロールで、"a" または "b" を入力できます。2 番目のパラメーターはオプション ボタンのグループで、値は "c"、"d"、"e"、または "f" のいずれかになります。3 番目のパラメーターはコンボボックス コントロールで、値は "g"、"h"、または "i" のいずれかです。4 番目のパラメーターは、チェックボックス コントロールで、値は "j" または "k" のどちらかです。したがって、テストケースとして入力するセットの 1 つは { "a", "c", "g", "j" } になるでしょう。このダミー アプリケーションの場合、入力のセットして考えられる組み合わせは合計 2 * 4 * 3 * 2 = 48 とおりになり、管理できる範囲です。しかし、5 つのパラメーターを受け取り、この各パラメーターが 52 個の値 (通常のトランプ 1 組の枚数) のいずれかになる、トランプ ゲーム アプリケーションを想像してみてください。この場合の入力セット数は 52 * 52 * 52 * 52 * 52 = 380,204,032 とおりになります。これは管理できる範囲ではなく、テストの入力セットごとに予想される値をプログラムで生成する必要があります。

A Dummy Application with Four Input Parameters

図 1 4 つの入力パラメーターを受け取るダミー アプリケーション

ペアワイズ テストの考え方は、各パラメーターのパラメータ値から、可能性のあるすべてのペアを網羅するテスト セットのリストを生成することです。図 1 の例では、このような入力ペアの合計は以下の 44 とおりです。

(a,c), (a,d), (a,e), (a,f), (a,g), (a,h), (a,i), (a,j), (a,k), (b,c), (b,d), (b,e), (b,f), (b,g), (b,h), (b,i), (b,j), (b,k), (c,g), (c,h), (c,i), (c,j), (c,k), (d,g), (d,h), (d,i), (d,j), (d,k), (e,g), (e,h), (e,i), (e,j), (e,k), (f,g), (f,h), (f,i), (f,j), (f,k), (g,j), (g,k), (h,j), (h,k), (i,j), (i,k)

ここで、テスト セット { "a", "c", "g", "j" } は 44 組のペアのうち (a,c)、(a,g)、(a,j)、(c,g)、(c,j)、(g,j) の 6 組を使用します。ペアワイズ テスト セットを生成する際の目標は、全 44 組のペアを網羅するテスト セットのコレクションを作成することです。図 2 のスクリーンショットをご覧ください。

Pairwise Test Set Generation with the QICT Tool

図 2 QICT ツールで生成したペアワイズ テスト セット

このスクリーンショットでは、qict.exe というツールを使って、図 1 に示したシナリオの全 44 組の入力ペアを網羅する 12 個のテスト セットのコレクションを生成しているようすを示しています。図 2 で生成されている 12 個のテスト セットの各値のペアを詳しく調べてみると、上記の 44 組のペアがすべて網羅されていることがわかるでしょう。この場合、考えられる 48 個のテスト ケース数が 12 個に削減されたことになります。このような小さな例でのこの程度の削減はそれほど大きくは感じられませんが、これから紹介する例をご覧になれば、多くの状況でペアワイズ テストによってテストケースの入力数が激減することをおわかりいただけるでしょう。ペアワイズ テストの根拠となっている前提は、ある特定のパラメーターからの値を使用するコードよりも、さまざまなパラメーターからの値を組み合わせて使用するコードの方にソフトウェアのバグが多く見つかっているという点にあります。つまり、図 1 のダミー アプリケーションで言えば、1 つのコントロールからの "a" と "b" を扱うコードよりも、複数のコントロールからの "a" と "g" を扱うコードの方がロジック エラーを生じやすいということです。これは、実際のいくつかの調査に基づいた考え方です。

PICT ツールを使用する

ペアワイズ テスト セットの生成に利用できるツールはいくつかあります。多くの状況で私が好んで使用するのは PICT (Pairwise Independent Combinatorial Testing) ツールです。PICT は私の同僚の Jacek Czerwonka が作成したものです。Jacek は、マイクロソフト社内の既存のペアワイズ ツールのコードを改良してこのツールを作成しました。PICT はいくつかの場所から無償でダウンロードできます。その 1 つが Microsoft Tester Center (msdn.microsoft.com/testing/bb980925.aspx、英語) です。インターネットを検索してみると、他にもいくつかペアワイズ テスト セット生成ツールが見つかります。PICT は、シェル コマンド ラインから実行する単一の実行可能ファイルです。非常に高速かつ強力で、大半の状況でのペアワイズ テストのニーズを満たしています。私は PCIT ツールの重要性を認めるために、このコラムで紹介するツールに QICT と名付けました。QICT は特別何かを略した名称ではありません。

では、なぜ、わざわざ別の生成ツールを用意したのでしょう。理由はいくつかあります。まず、PICT はすばらしいツールですが、ネイティブ C++ コードで作成されていて、ソース コードを入手できません。このコラムで紹介する QICT ツールは、私が知る限り、マネージ C# コードで作成され、製品レベルの品質を備えた最初のペアワイズ ツールです。コードを無償で入手できるため、独自のニーズに合わせて QICT を変更できます。たとえば、入力を XML ファイルや SQL データベースから直接読み取るように変更したり、結果を独自の出力形式で出力するように変更したりできます。また、制約 (許可されないテスト入力セット) を導入したり、必須のテスト セットを導入したり、テスト セットのコレクションの生成方法を変えてみたりと、ツールのロジックを利用してさまざな試みを行うことも考えられます。QICT のソース コードを利用して、ペアワイズ テスト セット生成コードを .NET アプリケーションやテスト ツールに直接コピーしたり、組み込んだりすることもできます。インターネット上にソース コードを公開しているペアワイズ テスト セット生成ツールもいくつかありますが、これらのツールのいくつかは機能が明らかに不足しています。たとえば、20 個のパラメーターがあり、各パラメーターが 10 個の値を取り得るとしましょう。このシナリオでは、10 * 10 * 10 * . . . * 10 (20 回) = 10^20 = 100,000,000,000,000,000,000 個のテストケース入力が考えられます。これは非常に大量のテストケース数です。PICT ツールはペアワイズ テスト セット数を 217 個まで削減します。QICT ツールは 219 個または 216 個 (乱数発生ツールのシード値によって変化します。この後説明します) まで削減します。しかし、幅広く利用されている Perl で記述されたペアワイズ テスト セット ツールでは 664 個のテスト セットが生成されます。最後に、QICT のソース コードを入手でき、このコラムで使用しているアルゴリズムを説明していることから、QICT を Perl、Python、Java、JavaScript といった好みの言語で書き直すことも可能です。

QICT ツール

QICT ツールのコードをすべてこのコラムで紹介するにはやや長すぎます。そこで、全ソース コードは MSDN Code Gallery (code.msdn.microsoft.com、英語) から入手できるようにしました。このコラムでは、ツールに使用しているアルゴリズムとデータ構造を、重要なコードのスニペットを添えて説明しているため、QICT を使用および変更するために必要な情報はすべて得られるでしょう。QICT のしくみの基本的な考え方は、可能性のあるすべてのペアが網羅されるまで各パラメーター値を配置する最長一致のアルゴリズムを使用して、一度に 1 つのテスト セットを生成することです。QICT の大まかなアルゴリズムを図 3 に示します。

図 3 QICT のアルゴリズム

read input file
create internal data structures

create an empty testset collection
while (number of unused pairs > 0)
  for i := 1 to candidate poolSize
    create an empty candidate testset
    pick the "best" unused pair
    place best pair values into testset
    foreach remaining parameter position
      pick a "best" parameter value
      place the best value into testset
    end foreach
  end for
  determine "best" candidate testset
  add best testset to testset collection
  update unused pairs list
end while

display testset collection

この大まかなアルゴリズムを実装する際に重要なことは、どのようなデータ構造を使用するかと、"best (最善)" のオプションとは何かを決定することです。QICT は次のようなソース コードから始まります。

static void Main(string[] args)
{
  string file = args[0];
  Random r = new Random(2);
  int numberParameters = 0;
  int numberParameterValues = 0;
  int numberPairs = 0;
  int poolSize = 20;

QICT のコーディングは、オブジェクト指向のアプローチではなく、従来の手続き型のスタイルを使用しています。これは、Perl や JavaScript といった OOP サポートが限られている言語を使って容易に QICT を書き直せるようにするためです。まず、コマンド ラインから入力ファイルを読み取ります。おわかりのように、コードを簡潔にするために、通常のエラー チェック処理を含めていません。QICT の入力ファイルは PICT で使用しているのと同じ、次のような単純なテキスト ファイルです。

Param0: a, b
Param1: c, d, e, f
など

パラメーター名の後にコロン (:) を付け、その後にそのパラメーターの正規の値をコンマで区切って指定します。パラメーター値はすべて異なっていなければなりません。次に、Random オブジェクトのインスタンスを作成します。シード値には 2 を選択しましたが、この値は自由に変更できます。ただし、どのような値を指定しても、入力セットが同じであれば QICT を実行するたびに同じ結果が生成されます。この擬似乱数オブジェクトの目的はいずれ説明します。続いて、入力ファイルを読み取るときに値を代入する 3 つの変数を宣言しています。図 2 の例では、numberParameters は 4、numberParameterValues は 11、numberPairs は 44 です。poolSize 変数は、テスト セットごとに生成するテスト セットの候補数を格納します。QICT を少し試してみると、poolSize 値を調整するという実に簡単な操作だけでツールに大きな影響を与えることがわかります。QICT で最も重要な部分は、メインのデータ構造宣言です。最初に以下の 4 つのオブジェクトを宣言します。

 

int[][] legalValues = null;
string[] parameterValues = null;
int[,] allPairsDisplay = null;
List<int[]> unusedPairs = null;

legalValues オブジェクトは、各セルに順次 int 値の配列を保持するジャグ配列 (多次元配列) です。legalValues 配列は、入力ファイルのメモリ内表現を保持します。つまり、legalValues のセル 0 は、値 0 (パラメーター値 "a" のメモリ内表現) と値 1 ("b" のメモリ内表現) を順番に格納した配列を保持します。文字列値を直接操作するのは非効率で、パラメーター値を整数で表現する方が、はるかに高いパフォーマンスが生み出されます。parameterValues は文字列の配列で、実際のパラメーター値を保持します。QICT の最後にこれらの値を使って、結果を整数値ではなく、文字列で表示します。つまり、上記の例では、セル 0 が "a"、セル 1 が "b" というように続き、セル 10 が "k" を保持します。allPairsDisplay オブジェクトは、int 型の 2 次元配列で、可能性のあるすべてのペアを設定します。この例では、最初のペアとして、セル [0,0] に 0 ("a")、セル [0,1] に 2 ("c") を保持します。2 番目のペア (a,d) として、セル [1,0] に 0、セル [1,1] に 3 を保持します。unusedPairs オブジェクトは、int 型配列のジェネリック リストです。unusedPairs の先頭項目は最初は {0,2} です。unusedPairs に配列ではなくリスト コレクションを使用しているのは、テスト セットのコレクションに新しいテスト セットを追加するたびに、その新しいテスト セットによって生成されるペアを unusedPairs から削除するためです。また、リストを使用すれば、unusedPairs.Count が 0 になったときに、簡単に処理を停止できるためです。

メイン プログラムの次のデータ構造は、以下の 4 つです。

int[,] unusedPairsSearch = null;
int[] parameterPositions = null;
int[] unusedCounts = null;
List<int[]> testSets = null;

QICT を始めとするほとんどのペアワイズ テスト セット生成ツールでは、大量の検索を行います。高いパフォーマンスを得るには、検索処理の効率がきわめて重要です。ここでは、unusedPairsSearch という 2 次元配列を宣言しています。これは縦と横のサイズがそれぞれ numberParameterValues の正方配列で、各セルは、対応するペアが使用されていない場合は 1、既に使用されているか、無効なペアの場合は 1 を保持します。図 2 の例では、unusedPairsSearch の最初の 3 行が次のようになります。

0 0 1 1 1 1 1 1 1 1 1
0 0 1 1 1 1 1 1 1 1 1
0 0 0 0 0 0 1 1 1 1 1

この後も、同じように続きます。

最初の行の (0,0) と (0,1) は (a,a) と (a,b) を表し、無効なペアなので 0 が設定されています。(0,2)、(0,3) ~ (0,10) は (a,c)、(a,d) ~ (a,k) を表し、まだテスト セットに使用されていないため 1 が設定されています。parameterPositions 配列は、テスト セット内での指定されたパラメーター値の場所を保持します。この配列を初期化した後は、次のようになります。

0 0 1 1 1 1 2 2 2 3 3

parameterPositions のインデックスはパラメーター値を表し、対応するセル値がテスト セット内での位置を表します。つまり、左から 4 番目のセルは、インデックスに 3、値に 1 を保持しますが、これはパラメーター値 3 ("d") がテスト セット内の位置 1 (2 番目のスロット) にあることを意味します。unusedCounts オブジェクトは、unusedPairs array 内で特定のパラメーター値が出現する回数を保持する 1 次元配列です。unusedCounts は、最初に以下の値を保持します。

9 9 7 7 7 7 8 8 8 9 9

インデックスはパラメーター値を表し、対応するセル値は未使用の数です。つまり、左から 4 番目のセルは、インデックスに 3、値に 7 を保持しますが、これはパラメーター値 3("d") が最初は未使用の 7 つのペア (a,d)、(b,d)、(d,g)、(d,h)、(d,i)、(d,j)、および (d,k) に出現していることを表します。testSets オブジェクトは、ペアワイズ テスト セットの結果を保持します。最初は空で、新しいテスト セットが生成されるたびに拡張されます。各テスト セットは、int 型の配列で表現します。図 2 の例では、結果の最初のテスト セット {"a", "c", "g", "j" } は testSets リストには値 {0,2,6,9} の配列として格納されます。

主要データ構造を適切に設定すると、 QICT は入力ファイルを読み取り、numberParameters と numberParameterValues の値を決定し、legalValues 配列と parameterValues 配列を設定します。ここでは比較的おおざっぱな手法を用い、最初のパスでファイルを読み取り、ファイル ポインターをリセットして、ファイル全体に対して 2 回目のパスを実行しています。legalValues 配列を設定したら、この配列をスキャンして、次のように入力のペア数を決定します。

for (int i = 0; i <= legalValues.Length - 2; ++i) {
  for (int j = i + 1; j <= legalValues.Length - 1; ++j) {
    numberPairs += (legalValues[i].Length * legalValues[j].Length);
  }
}
Console.WriteLine("\nThere are " + numberPairs + " pairs ");

初期化が終了すると、legalValues の最初の行は {0,1} を保持し、2 行目は {2,3,4,5} を保持します。この 2 行から決まるペアは (0,2)、(0,3)、(0,4)、(0,5)、(1,2)、(1,3)、(1,4)、および (1,5) です。一般に、legalValues の任意の 2 行から決まるペア数は、各行の値の数 (その行の Length プロパティ) どうしの積になります。次に、unusedPairs リストを設定します。

unusedPairs = new List<int[]>();
for (int i = 0; i <= legalValues.Length - 2; ++i) {
  for (int j = i + 1; j <= legalValues.Length - 1; ++j) {
    int[] firstRow = legalValues[i];
    int[] secondRow = legalValues[j];
    for (int x = 0; x < firstRow.Length; ++x) {
      for (int y = 0; y < secondRow.Length; ++y) {
        int[] aPair = new int[2];
        aPair[0] = firstRow[x];
        aPair[1] = secondRow[y];
        unusedPairs.Add(aPair);
      }
    }
  }
}

ここでは、インデックス i と j を使って、legalValues の各行のペアを把握しています。次に、インデックス x と y を使って各行ペアの値をすべて処理しています。入れ子になった複数の for ループを多数使用するのは、こうした組み合わせコードの特徴です。このようなコードを記述するときは、私はいつも 1 枚の紙を用意し、関係するすべての配列を書き出しています。こうした図を用意しないと簡単にミスをおかしてしまいます。unusedPairs リストを設定したら、同じ入れ子のループ構造を使って、allPairsDisplay 配列と unusedPairsSearch 配列を設定します。次に行う初期化では、legalValues 全体を反復処理して、parameterPositions 配列を設定します。

parameterPositions = new int[numberParameterValues];
int k = 0;
for (int i = 0; i < legalValues.Length; ++i) {
  int[] curr = legalValues[i];
  for (int j = 0; j < curr.Length; ++j) {
    parameterPositions[k++] = i;
  }
}

初期化コードの最後は、unusedCounts 配列の設定です。

unusedCounts = new int[numberParameterValues];
for (int i = 0; i < allPairsDisplay.GetLength(0); ++i) {
  ++unusedCounts[allPairsDisplay[i, 0]];
  ++unusedCounts[allPairsDisplay[i, 1]];
}

この初期化や多くの QICT ルーチンでは、C# の int 型配列のすべてのセルが自動的に 0 に初期化されることを利用しています。QICT をオブジェクト指向のスタイルに書き直す場合は、これらの初期化ルーチンをすべて、オブジェクトのコンストラクターか、明示的な Initialize() メソッドで処理することをお勧めします。メーン処理のループを、次のように開始します。

testSets = new List<int[]>();
while (unusedPairs.Count > 0) {
  int[][] candidateSets = new int[poolSize][];
  for (int candidate = 0; candidate < poolSize; ++candidate) {
    int[] testSet = new int[numberParameters];
  // fill candidate testSets
  }
  // copy best testSet into testSets collection; upate data structues
}

候補テスト セットの数は poolSize に設定されていることがわかっているため、動的にサイズが決定されるリスト オブジェクトではなく、配列のインスタンスを作成できます。unusedPairs コレクションのサイズを使って、メイン処理ループの終了を制御しています。さあ、ここからが本番です。"最善" の未使用ペアを選択するという最も興味深い点に踏み込みます。

int bestWeight = 0;
int indexOfBestPair = 0;
for (int i = 0; i < unusedPairs.Count; ++i) {
  int[] curr = unusedPairs[i];
  int weight = unusedCounts[curr[0]] + unusedCounts[curr[1]];
  if (weight > bestWeight) {
    bestWeight = weight;
    indexOfBestPair = i;
  }
}

ここでは、個別のパラメーター値の未使用数の合計が最も大きな未使用ペアを "最善" と定義します。たとえば、現在の未使用ペアのリストの中で、"a" が 1 回、"b" が 2 回、"c" が 3 回、"d" が 4 回出現しているとします。この場合。ペア (a,c) の重みは 1 + 3 = 4、ペア (b,d) の重みは 2 + 4 = 6 となり、(a,c) よりも (b,d) が最善のペアとして選択されます。

少し調べてみれば、重み付けの手法はたくさん見つかります。たとえば、なんらかの乗算を用いて、互いの未使用数に差がないペアよりも、かけ離れた未使用数を持つペアの方に高い重みを付けることもできます。他にも、使用済み数 (結果の testSets コレクションに既に追加されたテスト セットにパラメーター値が出現する回数) を追跡し、使用済み回数が最も少ないペアを最善のペアとして選択する方法もあります。最善の未使用ペアを決定したら、そのペア値を保持する 2 つのセルから成る配列を作成し、テスト セット内で各値が所属する位置を決定します。

int[] best = new int[2];
unusedPairs[indexOfBestPair].CopyTo(best, 0);
int firstPos = parameterPositions[best[0]];
int secondPos = parameterPositions[best[1]];

この時点では、空のテスト セットとテスト セットに収める値のペアを用意しました。また、テスト セット内で値が所属する場所もわかっています。次の手順では、テスト セット内で残っている位置のパラメーター値を生成します。ここでは、テスト セットの位置を (インデックスの低い方から高い方といった) 固定的な順序ではなく、ランダムな順序で設定しています。この方が適切であることはいずれわかります。まず、パラメーターの位置を順番に保持する配列を生成します。

int[] ordering = new int[numberParameters];
for (int i = 0; i < numberParameters; ++i) 
  ordering[i] = i;

次に、最善のペアの最初の 2 つの値の既知の場所を、順序配列の最初の 2 つのセルに格納して、順序を再配置します。

ordering[0] = firstPos;
ordering[firstPos] = 0;
int t = ordering[1];
ordering[1] = secondPos;
ordering[secondPos] = t;

ここで、Knuth のシャッフル アルゴリズムを使って、残りの位置 (セル 2 以降) をシャッフルします。これが、QICT コードの先頭で Random オブジェクトを作成した理由です。QICT が生成するテスト セットの数は、驚くほど擬似乱数発生ツールのシード値の影響を受けます。そのため、いくつかのシード値を使って試してみることをお勧めします。コラムの冒頭に挙げたそれぞれ 10 個の値を持つ 20 個のパラメーターを使用する状況では、シード値 2 で 219 個、シード値 6 で 216 個、シード値 0 で 221 個のテスト セットが生成されました。

for (int i = 2; i < ordering.Length; i++) {
  int j = r.Next(i, ordering.Length);
  int temp = ordering[j];
  ordering[j] = ordering[i];
  ordering[i] = temp;
}

シャッフルしたら、最善のペアから 2 つの値を候補テスト セットに格納します。

testSet[firstPos] = best[0];
testSet[secondPos] = best[1];

さて、ここからが QICT アルゴリズムで最も重要な部分です。テスト セットの空いている場所それぞれに収める最善のパラメーター値を決定しなければなりません。ここで使用した技法も最長一致のアプローチです。パラメーターの位置ごとに、その位置で正規の値として可能性のある値をそれぞれテストします。そのためには、その位置に配置可能な値と、テスト セット内に既に取り込んだ他の値を組み合わせたときの、そのテスト値の未使用のペア数を数えます。その後、未使用のペアを最も多く網羅するパラメーター値を選択します。QICT の中で最も技巧を凝らしてこれを行っているコードを図 4 に示します。

図 4 テスト セットに最善のパラメーター値を設定する

for (int i = 2; i < numberParameters; ++i) {
  int currPos = ordering[i];
  int[] possibleValues = legalValues[currPos];
  int currentCount = 0;
  int highestCount = 0;
  int bestJ = 0; 
  for (int j = 0; j < possibleValues.Length; ++j) {
    currentCount = 0;
    for (int p = 0; p < i; ++p) {
      int[] candidatePair = new int[] { possibleValues[j],
        testSet[ordering[p]] };
      if (unusedPairsSearch[candidatePair[0], candidatePair[1]] == 1 ||
        unusedPairsSearch[candidatePair[1], candidatePair[0]] == 1)
        ++currentCount;
    }
    if (currentCount > highestCount) {
      highestCount = currentCount;
      bestJ = j;
    }
  }
  testSet[currPos] = possibleValues[bestJ];
}

図 4 の最も外側のループは、2 (最初の 2 つは最善のペアが使用しているため) から始まり、テスト セットの位置の合計数 (numberParameters) まで繰り返します。このループの内部で、以前に作成した順序配列から、設定する現在位置を割り出します。currentCount 変数は、テスト パラメーター値から計算された未使用ペア数を保持します。テスト セットの位置をランダムに設定しているため、値の候補ペアの順序も並んでいない可能性があります。したがって、unusedPairsSearch 配列を照合するときに、2 つの可能性をチェックする必要があります。図 4 のコードの最後で、各位置に最長一致アルゴリズムを使用して選択した値を含む候補テスト セットが得られます。ここで、次のようにこの候補テスト セットを単純に候補のコレクションに追加します。

candidateSets[candidate] = testSet;

この時点で、n = poolSize の候補テスト セットが得られます。そこで、これらの候補の中から本来の testSet 結果コレクションに追加する最善の候補を選択する必要があります。最初の候補テスト セットに最も未使用数の多いペアが取り込まれていると想定できるため、位置 0 から始めて単純に各候補を反復処理します。しかし、ここでもなんらかのランダム性を導入する方が適切な結果が生み出されます。ここでは、ランダムな位置にある候補を選択し、最善の候補と想定しています。

int indexOfBestCandidate = r.Next(candidateSets.Length);
int mostPairsCaptured =
  NumberPairsCaptured(candidateSets[indexOfBestCandidate],
    unusedPairsSearch);

ここで、特定のテスト セットに利用されている未使用ペアの数を判断するために、NumberPairsCaptured() という小さなヘルパー関数を使用しています。このヘルパー関数は以下のとおりです。

static int NumberPairsCaptured(int[] ts, int[,] unusedPairsSearch) 
{
  int ans = 0;
  for (int i = 0; i <= ts.Length - 2; ++i) {
    for (int j = i + 1; j <= ts.Length - 1; ++j) {
      if (unusedPairsSearch[ts[i], ts[j]] == 1)
        ++ans;
    }
  }
  return ans;
}

ここで、各候補テスト セットを調べ、最も未使用数の多いペアを取り込んだテスト セットの場所を追跡します。

for (int i = 0; i < candidateSets.Length; ++i) {
  int pairsCaptured = NumberPairsCaptured(candidateSets[i],
    unusedPairsSearch);
  if (pairsCaptured > mostPairsCaptured) {
    mostPairsCaptured = pairsCaptured;
    indexOfBestCandidate = i;
  }
}

最善の候補テスト セットをメインの結果 testSets リスト オブジェクトにコピーします。

int[] bestTestSet = new int[numberParameters];
candidateSets[indexOfBestCandidate].CopyTo(bestTestSet, 0);
testSets.Add(bestTestSet);

この時点で、新しいテスト セットが生成および追加されたことになります。そこで、影響を受けるすべてのデータ構造を更新する必要があります。主に対象となるのは、unusedPairs リスト (新しいテスト セットによって生成されるすべてのペアを削除します)、unusedCounts 配列 (新しいテスト セットの各パラメーター値のカウントを減少します)、および unusedPairsSearch 行列 (新しいテスト セットによって生成される各ペアに関連する値を 1 から 0 に変更します) です。

これで、メイン処理ループは終わりまできました。ここから、先頭に戻り、候補の生成、最善の候補の選択、最善の候補の testSets への追加、およびデータ構造の更新の各操作を続けます。未使用ペアの数が 0 になったら、この処理が終了します。

最後に、最終結果を表示します。

Console.WriteLine("\nResult testsets: \n");
for (int i = 0; i < testSets.Count; ++i) {
  Console.Write(i.ToString().PadLeft(3) + ": ");
  int[] curr = testSets[i];
  for (int j = 0; j < numberParameters; ++j) {
    Console.Write(parameterValues[curr[j]] + " ");
  }
  Console.WriteLine("");
}
  Console.WriteLine("");
}

冒頭に述べたように、独自のテスト シナリオに合うように QICT を変更して、結果を XML ファイルや SQL データベースなどの他の形式のストレージに直接出力することもできます。

適切なシステムを生み出すために

ペアワイズ テストは、確率的な要因を備えた組み合わせ手法です。ペアワイズ テスト セットの生成は重要な手法ではありますが、マジックではありません。ペアワイズとは、扱うテストケースの数が多すぎるような状況で、単にテストケースの入力数を削減する手法です。ペアワイズ テスト セット生成によって、テストケースに想定される結果が作り出されるわけではありません。テストを行う場合は、境界条件を調べるような通常のテストの原則から着手し、純粋なランダム入力などのテストを行ってから、テストケース生成の補助手段としてペアワイズ テストを使用してください。また、大まかな経験則から言えば、たくさんのテストを行うに越したことはありません。したがって、ペアワイズ生成ツールが生成したテストケースの入力にテストケースを追加しても何の問題もありません。ペアワイズ テストは多くの状況に役立ちますが、適切な場合にのみ使用するようにしてください。

私は、ペアワイズ テスト セット生成が、構成のテスト、列挙値を受け取るモジュール テスト手法、およびテーブルの各列が比較的少数の異なる値を持つような SQL データベースのテストに非常に有効であることを発見しました。テストケースの入力が比較的少ないシナリオや、テストケースの想定結果をプログラムで生成するため、扱うテストケースの入力数が多くなるような状況では、ペアワイズ テストが必ずしも優れたアプローチになるとは言えません。また、テスト対象のシステムへの入力値が不連続な場合もあまり有効なアプローチではありません。ただし、可能性のあるパラメーター値の数が膨大になるような状況でも、パラメーター値を同値類に分離すれば、ペアワイズ テストケース入力生成を効果的に使用することができます。ペアワイズ テスト セット生成は、正しく使用すれば、より適切なソフトウェア システムを作成するのに役立つ重要な技法になります。

Dr. James McCaffrey は Volt Information Sciences, Inc. に勤務し、ワシントン州レドモンドにあるマイクロソフト本社で働くソフトウェア エンジニアの技術トレーニングを管理しています。これまでに、Internet Explorer、MSN サーチなどの複数のマイクロソフト製品にも携わってきました。また、『.NET Test Automation:A Problem-Solution Approach』 (Apress、2006) の著者でもあります。連絡先は、jmccaffrey@volt.com または v-jammc@microsoft.com です。

この記事のレビューに協力してくれた技術スタッフの Jacek Czerwonka に心より感謝いたします。

ご意見やご質問は、James (testrun@microsoft.com) まで英語でお送りください。