次の方法で共有


ミューテーションの力

.NET Framework による単純なミューテーション テスト システムの作成

James McCaffrey


この記事で取り上げる話題:

  • ミューテーション テストおよびコード カバレッジとの関連性の理解
  • テストが行われる DLL の準備およびミューテーション
  • テスト結果の分析

この記事で使用する技術:

  • .NET Framework、C#

サンプルコードのダウンロード: MutationTesting.exe (353KB)
翻訳元: Create A Simple Mutation Testing System With The .NET Framework (英語)


目次

  1. DLL およびテスト ハーネス
  2. ミューテーション テスト システム
  3. ミューテーション分析の準備
  4. 主要なミューテーション ループ
  5. ミューテーション分析の結果
  6. まとめ

ミューテーション テストは、最も一般的ではなく最も誤解されている種類のテスト分析です。ミューテーション テストを使用すると、テストが行われるシステムあるいはプログラムを変更して、ミュータントと呼ばれる欠点のあるバージョンを作成します。そして、テスト ケース スイートを使用して、新しいテスト ケースの失敗を引き起こすと思われるミュータント プログラムを実行します。新たに失敗が発生しない場合は、テスト スイートがミューテーションが発生したコードを含むコード パスを実行していない可能性が高く、これはプログラムが完全にテストされていないことを意味します。そのため、ミュータント コードを実行する新しいテスト ケースを作成します。

技術的問題が困難でリソース集約的であると考えられているため、ミューテーション テストはあまり使用されません。Microsoft .NET Framework によって、ミューテーション テストの実行がよりいっそう簡単になることが、.NET Framework の利点の 1 つです。この記事では、C# 言語に実装されている簡単かつ完全なミューテーション テストの技術について示します。

軽量な .NET Framework ミューテーション分析について見てみましょう。図 1 は、LibUnderTest.dll という名前のライブラリを分析しているところを示します。これは 1 つの Max メソッドのみを含むデモ版のライブラリです。ミューテーション分析の最初の部分では、TestHarness.exe という既存のテスト スイートが DLL 上で実行されます。簡単にするために、テスト スイートは 10 個のテスト ケースのみが含まれています。10 個のテスト ケースがパスしたことに注意してください。後ほど説明するように、ミューテーション分析は、プログラムに対して実行したときにパスしたテスト ケースのみを含むテスト スイートを使用して開始したいと考えるのが一般的です。

図 1 ミューテーション分析の出力
図 1 ミューテーション分析の出力

基準となるテスト結果を構築後、テストが行われる DLL にミューテーションが起こる可能性があるかどうかを調査します。これが、.NET Framework を使用して行うとよりいっそう簡単になるタスクのうちの 1 つです。テストが行われるシステムあるいはプログラムのミューテーションの可能性を調査する方法について後ほど説明します。今は、テストが行われる DLL の逆アセンブリを行い、分岐命令について結果の .il ファイルを調査すると述べるだけで十分です。この例では、DLL には 3 つの ble (より小さいか等しければ分岐) 命令があります。そして、ミューテーション分析は主要な処理のループに入ります。ループのパスごとに、もともとの DLL でミューテーションが発生し、新しい DLL を作成します。新しいミュータント DLL はビルドされ、テスト スイートはこのミュータントに対して実行します。

この例では、1 番目のミュータントはテストの失敗を引き起こしませんでした。これは良くありません。テスト スイートはミューテーションが発生したコードに含まれるコード パスを見逃した可能性があります。2 番目および 3 番目のミュータントは、テスト ケースの失敗を (うまく) 引き起こし、テスト スイートのベータ能力に役立ちます。ミューテーション分析は、関連するミューテーション アルファおよびベータ能力を出力することで終了します。アルファ能力 (この例では 0.67) は、テスト ケースの失敗を引き起こすミューテーションの割合に過ぎません。ベータ能力については後ほど説明します。

この記事では、テストが行われる DLL について簡単に説明します。そして、図 1 に示される結果を引き起こす単純なミューテーション分析コードを示します。自分のニーズを満たすように、ミューテーション システムを修正および拡張することができます。ミューテーション テストの使用方法および使用タイミング、そして他の種類のテスト (特にコード カバレッジ) との関連に関して非常に簡単に検討を行って終わります。


1. DLL およびテスト ハーネス

物事を簡単にしておくために、テストが行われる DLL にはインスタンス メソッド Max をもつ 1 つの Utility クラスを含めます。

public int Max(int a, int b, int c)
{
    if (a > b && a > c) return a;
    else if (b > c) return b;
    else return c;
}

メソッド Max は正しく実装されていますが、現実の世界では、正しく実装されていると仮定することができません。仮定できるのであれば、テストを行う必要などまったくないでしょう。

この記事向けに、最大限簡単にするよう設計されたデモ版のテスト ハーネスを作成しました (図 2 を参照)。テスト ハーネスには、10 個のハードコードされたテスト ケースがあることに注意してください。テスト ケースごとにテスト ケース ID、装置にどのメソッドをテストするかを指示するフィールド、Max に対する 3 つの入力引数、および予測される結果が含まれます。これらのケースが Max メソッドのテストを十分に行わないことに気づくでしょう。10 個のケースすべてにおいて、最大値は 3 番目の入力引数です。小さい例で確認するのは簡単ですが、何千ものテスト ケースがある本番環境では、テスト ケースのスイートがコードを十分にテストするかどうかは明らかではありません。まさにこのことを判断するために、ミューテーション テストは設計されます。

テスト ハーネスは、各テスト ケースを繰り返し、各テスト ケース フィールドを解析し、LibUnderTest オブジェクトのインスタンスを作成し、テスト ケース入力を指定してメソッド Max を呼び出し、実際の結果と予測される結果を比較してテスト ケースがパスするか、失敗するかどうかを判断します。テスト ハーネスは、Results.txt という名前の外部ファイルに結果を出力します。このファイルは 1 行の、カンマ区切りの形式で、パスしたケースの数、失敗したケースの数、およびテストの実行が終了した時間を保存します。結果ファイルは次のようになります。

8,2,9:55 AM

ミューテーション テストを行う場合、おそらく既存のテスト スイートおよびテスト ハーネスを継承するでしょう。テスト ハーネスをプログラムで実行してテスト実行の結果を取得する限り、ミューテーション テストを行うことができます。プログラムで結果を取得できないようにテスト ハーネスが設計されている場合 (純粋な GUI テスト フレームワークなど) のみ問題が発生します。テスト ハーネスを設計しているのであれば、より自動化しやすいように装置を作成すると、ミューテーション テスト システムの作成もより簡単になることを覚えておいてください。

ページのトップへ


2. ミューテーション テスト システム

図 1 に示す出力を生じさせるミューテーション プログラムについて順を追って説明します。プログラムの高レベルの構造は、図 3 に示されます。プログラム全体は、この記事に付随するコード ダウンロードにあります。

図 3 ミューテーション テスト プログラム構造

static string[] brInstructions = { "beq", "bge", "bge.un", "bgt",
  "bgt.un", "ble", "ble.un", "blt", "blt.un", "bne.un" };
static string[] brComplements  = { "bne.un", "blt", "blt.un", "ble",
  "ble.un", "bgt", "bgt.un", "bge", "bge.un", "beq"    };
        
[STAThread]
static void Main(string[] args)
{
    Console.WriteLine(
        "\nBeginning mutation analysis of: LibUnderTest.dll");

    // もとの DLL を既定の場所にコピーする。
    // テスト ハーネスを、テストが行われる、ミューテーションが発生していないもとの DLL に対して実行する。
    // ildasm を使用して、ファイル LibUnderTest.il を作成する。
    // ファイル LibUnderTest.il をメモリ内の ArrayList に読み込む。
    // ミューテーションが発生する可能性のある行を探して、
    // その情報を Info オブジェクトの ArrayList に保存する。
    // listInfo の一覧を検証する。

    for (int i = 0; i < listInfo.Count; ++i) // main loop
    {
        // ミューテーションが発生した、インメモリの、テストが行われる DLL の ASM バージョンを作成する。
        // そして、新しい DLL をミューテーションからビルドする。
        // テスト ハーネスを実行して、ミューテーションが発生した DLL からテスト結果を読み込む。
    } 

    // ミューテーション テストの結果を表示する。
    Console.WriteLine("\nDone");
 }

ミューテーション分析プログラムは、2 つの主要な文字列配列である brInstructions および brComplements を宣言することから始まります。ミューテーション テスト システムを書いているときに最初に直面する問題は、ミューテーションさせたいものを厳密に決定することです。テスト スイートがミューテーションが発生したコードを実行するときに失敗するために、テストが行われるミュータント コードが必要であることを覚えておいてください。

テキスト文字列、+ や - といった算術演算子など、何でもミューテーションさせることができます。厳密に何をミューテーションさせなければならないかという議題に関する調査は多くありますが、論理的および実用的な観点から妥当な妥協点は、>、<、および == などの Boolean 比較演算子をミューテーションさせることです。

何をミューテーションさせるかを決定したら、ミューテーションを行う方法を決定する必要があります。.NET Framework 以前では、テストが行われるコードを解析するルーチンを記述して Boolean 演算子を検索することが、多くの場合には唯一の選択肢でした。これは非常に難しいです。さいわいにも、.NET Framework では、ミューテーション テストを記述するのがより簡単になっています。その理由は、.NET ベースのプログラムはコンパイルされて中間言語 (IL) となり、C# あるいは Visual Basic のソース コードではなく、より簡単な IL を解析することができるためです。

小さい例を用いて、説明します。テストが行われるシステムに、次のようなステートメントが含まれているとします。

if (x <= y) ++z;

<= のソース コードを解析するのは考えているより容易ではありません。なぜなら、コメント内なのか、文字列リテラルかどうかなどを気にしなくてはならないためです。ですが、一見簡単そうなソース コードに対応する IL は、次のようなものです。

IL_0006:  ldloc.0 // x
IL_0007:  ldloc.1 // y
IL_0008:  bgt.s      IL_000e
IL_000a:  ldloc.2 // z
IL_000b:  ldc.i4.1
IL_000c:  add
IL_000d:  stloc.2 // z = z+1
IL_000e:  ...

ソース コード内の <= Boolean 演算子は、IL では bgt 命令として解釈されます (より大きければ分岐を意味します)。<= は、ble (より小さいか等しければ分岐) と解釈されると予測したかもしれませんが、コンパイラは通常、Boolean 演算子を IL の補数分岐命令に解釈することで、制御フローを最適化することが分かります。テストが行われているシステムの IL を取得することができ、Boolean 演算子を解析することで IL 分岐命令が削減できれば、これは比較的簡単です。

最後の問題は、厳密にどの IL 分岐命令を何にミューテーションさせるかを決定することです。『IL reference guide (英語)(PDF、3.2MB)』には、図 4 に示すように、12 種類の分岐命令が一覧表示されています。

図 4 一般的な中間言語 (IL) 分岐命令

IL 分岐命令 命令タイトル 補数
beq 等しければ分岐 bne.un
bge より大きいか等しければ分岐 blt
bge.un より大きいか等しければ分岐 (符号なし) blt.un
bgt より大きければ分岐 ble
bgt.un より大きければ分岐 (符号なし) ble.un
ble より小さいか等しければ分岐 bgt
ble.un より小さいか等しければ分岐 (符号なし) bgt.un
blt より小さければ分岐 bge
blt.un より小さければ分岐 (符号なし) bge.un
bne.un 等しくなければ分岐 beq
brfalse false、null、あるいはゼロであれば分岐 brtue
brtrue false ではないか、null ではなければ分岐 brfalse

テスト ケースを失敗させるため、各 IL 分岐命令を論理的補数にミューテーションさせるのは納得できます。したがって、bgt は ble などにミューテーションされます。

この方法を使用しても、ミューテーションがテスト ケースの失敗を引き起こすことを完全に保証するものではないということを、ここで指摘します。テストが行われるシステムには、次のようなソース コードが含まれているとします。

if (s > t) i = i;

IL の対応するものを > 演算子にミューテーションさせても、テスト ケースの結果には何の影響も与えません。

この知識をもとに、brInstructions および brComplements 配列を宣言して、分岐命令を受け入れて、補数/ミューテーションを返す、次の小さな置換メソッドを記述します。

static string mutatedInstruction(string instruction)
{
  for (int i = 0; i < brInstructions.Length; ++i)
    if (brInstructions[i] == instruction)
      return brComplements[i];
  return null;
}

この記事のすべてのサンプルのように、エラーチェック コードのほとんどを削除して、できる限り要点を明らかに保つようにしました。しかし、本番環境では自由にエラーチェック コードを追加してください。ここで、brInstructions 配列をトラバースして、対応するものを brComplements 配列に返します。brfalse および brtrue 命令を除外することを適宜に決定したことに注意してください。ここで示したミューテーション テスト システムの設計により、何をミューテーションさせるか、および何にミューテーションさせるかを簡単に変更することができます。

ページのトップへ


3. ミューテーション分析の準備

準備の第一段階として、テスト スイートをもともとの、ミューテーションが発生していない、テストが行われる DLL に対して実行します。こうすると、後の比較のための基準となる結果を確定することができます。ミューテーションが発生していない、テストが行われる DLL をドロップ ポイントから取得して、それをテスト ハーネスの実行ファイルと同じフォルダにコピーすることから開始します。

Console.WriteLine(
  "\nCopying original DLL under test into test harness folder");
File.Copy(
  "..\\..\\..\\LibUnderTest\\bin\\Debug\\LibUnderTest.dll",
  "..\\..\\..\\TestHarness\\bin\\Debug\\LibUnderTest.dll", true);

ここで少し気をつけなければなりません。テスト ハーネスが、テスト ハーネスの実行フォルダにある、テストが行われる DLL を参照しているという重要な仮定を行います。通常の場合のように、テスト ハーネスが Visual Studio で作成されている場合、プロジェクトがテストが行われる DLL を参照するようにします。プロジェクトの参照はファイル名によりバインドされ、プロジェクトが指し示す DLL をテスト ハーネスの実行フォルダに物理的にコピーします。テスト ハーネスが、装置の実行ファイルを含むフォルダではないフォルダを参照している場合、ミューテーション分析によりミューテーションが発生している DLL がテスト ハーネスの実行フォルダにコピーされるときに、もともとの、ミューテーションが発生していない DLL により上書きされる可能性があります。静的 File.Copy メソッドに True 引数を設定すると、コードは前回のミューテーション分析の既存の DLL を上書きします。

ミューテーション分析の準備における次の手順は、次に示すようなテスト ハーネスを実行することです。

Console.WriteLine(
  "\nRunning test suite on original DLL under test");
using(Process test = Process.Start(
  "..\\..\\..\\TestHarness\\bin\\Debug\\TestHarness.exe"))
{
    test.WaitForExit();
}

ここでは単に、静的な Process.Start メソッドを呼び出すだけで装置を実行しています。以前に説明したように、ミューテーション分析の設計は、既存のテスト ハーネスの設計により影響を受けます。ご存知のように、この場合はテスト ハーネスはその結果をテキスト ファイルに書き出すだけです。テスト ハーネスをもともとの DLL に対して実行したら、図 5 に示されるようなテストの実行結果を取得します。

図 5 テスト結果の取得

string result;
using(StreamReader sr = new StreamReader(
    "..\\..\\..\\TestHarness\\Results\\Results.txt");
{
    result = sr.ReadLine();
}
int basePass = int.Parse(result.Split(',')[0]);
int baseFail = int.Parse(result.Split(',')[1]);

Console.WriteLine(
  "Baseline number pass, fail = " + basePass + " " + baseFail);
if (baseFail > 0) Console.WriteLine(
    "** Warning: baseline should not contain test case failures **");

ここでは、手が込んだことは行われていません。ただ、StreamReader オブジェクトを使用して、テスト ケースの結果ファイルから 1 行ずつ読み込みます。結果の出力形式は次のようになることを思い出してください。

8,2,9:55 AM

カンマ文字の引数を指定して String.Split を呼び出す場合、パスしたテスト ケースの数は結果の文字列配列のインデックス位置 [0] に格納され、失敗したテスト ケースの数は、インデックス位置 [1] に格納されます。現実的なテスト ハーネスでは、場合によっては XML ファイルあるいは SQL Server データベースなどの、より複雑な出力が生じます。

この例全体で、ハードコードされたパスを使用していますが、おそらくパラメータ化したいと考えるでしょう。また、ヘルパ メソッドの使用を避け、説明をできる限り簡単にしようとしています。ここで示されたシステムを、ヘルパ メソッドに作成しなおしてもかまいません。たとえば、前に示したコード行を GetBaselineResults に再度要素化して、テスト ハーネスの結果のファイル名を表す文字列を受け取り、[0] にパスしたケースの数を保持し、[1] に失敗したテスト ケースの数を保持する int の配列を返すようにすることができます。呼び出しは、次のようになります。

int[] baseResults = GetBaselineResults(testResultFile);

ミューテーション分析の準備における次の手順は、テストが行われる DLL の IL 版を生じさせることです。さいわいにも、Visual Studio とともに出荷される ildasm.exe ツールを使用してこの処理を行うことができます。

using(Process p = Process.Start("ildasm.exe",
  "..\\..\\..\\TestHarness\\bin\\Debug\\LibUnderTest.dll " +
  "/OUT=..\\..\\LibUnderTest.il")) 
{
    p.WaitForExit();
}

Process.Start メソッドを使用して ildasm.exe を起動します。ildasm.exe がもともとの、ミューテーションが発生していない、テストが行われる DLL を指し示すようにして、/OUT スイッチを指定します。こうすることで、ildasm.exe に対して既定のグラフィカルなインターフェイスを使用せずに出力をテキスト ファイルに書き込むよう指示します。もともとの DLL を IL 版で LibUnderTest.il としてミューテーション分析システムのルート フォルダに保存します。次は、結果のテキスト ファイルから少し抜粋したものを示します。

.maxstack  2
.locals init (int32 V_0)
IL_0000:  ldarg.1
IL_0001:  ldarg.2
IL_0002:  ble.s      IL_000c

ミューテーション分析を記述することの技術的な詳細とは離れますが、処理するファイルおよびフォルダもまたたくさんあります。作成されたファイルやフォルダすべてを追跡する難しさを軽視しないでください。テストが行われるコード、テスト ハーネス、およびミューテーション システムという関連する 3 つのシステムそれぞれに、ソース ファイル、実行ファイル、入出力ファイル、設定ファイル、およびディレクトリ構造があります。

もともとの DLL の IL 逆アセンブリを作成して保存してから、IL 命令をメモリに読み込むことができます。

using(StreamReader sr = new StreamReader("..\\..\\LibUnderTest.il"))
{
    string line;
    ArrayList listLibUnderTestOrig = new ArrayList();
    while ((line = sr.ReadLine()) != null)
    {
        listLibUnderTestOrig.Add(line);
    }
}

StreamReader オブジェクトを使用して、IL ファイルをメモリに 1 行ごとに読み込み、listLibUnderTestOrig という名前の ArrayList オブジェクトに保存します。これは、このインメモリのデータ ストアをすべてのミューテーションの基準として使用するという考え方です (大規模なプロジェクトでは、すべてを読み込んで格納するのではなく、行ごとに個別に処理およびミューテーションを行うこともできます)。ArrayList オブジェクトを使用するのが簡単で有効的ですが、必要に応じて、テストが行われる DLL のもともとの IL コードを他の種類のデータ構造に格納してもかまいません。このミューテーション テストの技術には、言語依存であることの非常に大きな利点があることに注意してください。IL を使用して作業をすることで、C#、Visual Basic、および他の .NET 準拠の言語の間のすべてのソース コードで差が生じないようにすることができます。

ミューテーション分析の準備における次の手順は、もともとの DLL の IL コードを調査して、ミューテーションが発生する可能性のある IL 命令を含む行を検索します。この可能性のあるミューテーション情報を図 6 のコードに示されるように lsitinfo という名前の ArrayList オブジェクトに格納します。

図 6 可能性のあるミューテーションの IL の分析

ArrayList listInfo = new ArrayList();
string currMethod = "unknown";
for (int i = 0; i < listLibUnderTestOrig.Count; ++i)
{
  string s = (string)listLibUnderTestOrig[i];

  if (s.IndexOf(".method") >= 0)
    currMethod = (string)listLibUnderTestOrig[i+1].ToString().Trim();

  for (int j = 0; j < brInstructions.Length; ++j)
  {
    if (s.IndexOf(":  " + brInstructions[j]) >= 0)
    {
      Info info = new Info(i, currMethod, brInstructions[j]); 
      listInfo.Add(info); // lineNum、メソッド、分岐命令
      break;
    }
  }
}

IL 命令を listLibUnderTestOrig という名前の ArrayList に保存したため、一度にコレクションの 1 つのオブジェクト (文字列) を単に繰り返します。必ずしも必要というわけではないですが、ミューテーション可能な命令がどの行で発生しているかを追跡することに加えて、その命令がどのメソッド内で発生しているかも追跡していると非常に便利です。ildasm.exe により生じた IL コード内で、すべてのメソッドの先頭に .method タグがついています。

.method public hidebysig instance int32 
        Max(int32 a, int32 b, int32 c) cil managed
{
  // コード サイズ       26 (0x1a)
  .maxstack  2
  .locals init (int32 V_0)
   IL_0000:  ldarg.1
...
   IL_0019:  ret
} // end of method Utility::Max

メソッド名は通常 (ですが常にではない) .method 文字列を含む行に続くことに注意してください。そのため、.method の行に到達するたびに、次の行を取得する場合は、通常現在のメソッドの署名の最初の一部を取得します。これは、非常に軽量な方法です。つまり、強固なソリューションでは、より高度な文字列解析のロジックが組まれるでしょう。

メソッド名の情報について必要であれば、ildasm.exe は便宜上各メソッドを、メソッド名を含むがパラメータ一覧あるいは戻り値の種類は含まない "// end of method" で始まるコメントで終了させるという事実を利用することができます。まず初めに IL コードを通過して、各メソッドの開始および終了行の行番号を決定します。そして、IL コードを 2 度目に通過時に .method 文字列に到達すると、その時点でどの行にいるかが分かり、どのメソッド内にいるかを調べることができます。

テストが行われる DLL の IL コードを分析するときに、brInstructions 文字列配列の分岐命令を検索します。これらの命令の 1 つが見つかった場合、発見された命令、命令が発生した行番号、および命令が発生したメソッド名を取得します。ミューテーションを実際に行うときのために、この情報を保存します。IL 命令、行番号、およびメソッドのフィールドを持つ、非常に軽量なオブジェクトを作成する簡単な方法は、そのオブジェクトを ArrayList に保存することです。このような軽量なクラスを次のように定義します。

class Info
{
  public int lineNum;
  public string method instruction;

  public Info(int lineNum, string method, string instruction)
  {
    this.lineNum = lineNum;
    this.method = method;
    this.instruction = instruction;
  }
}

これは、取得する必要がある最低限の量の情報だと思われます。現在の IL オフセット、親クラス名、あるいは分岐命令に関連するオペランドなどの追加情報が必要かもしれません。それは、各 Info オブジェクトが 1 つの IL 分岐命令を保持し、listInfo.Count プロパティは、可能性のあるミューテーションの数を返すためです。この例では、すべての可能性のあるミューテーションを実行しています。巨大なテスト スイートを持つ大規模なプロジェクトでは、これは実行可能ではないかもしれません。ここで採用する方法は、すべてではなく、ランダムに選択された分岐命令をミューテーションさせるというものです。この考え方の詳細について、次のセクションで説明します。

ミューテーション分析の準備処理における最後の手順は、可能性のあるミューテーションに関する情報を表示することです。

for (int i = 0; i < listInfo.Count; ++i)
{
  Info info = (Info)listInfo[i];
  Console.WriteLine(
    "On line " + info.lineNum + " inside method " +
    info.method + " found mutatable instruction '" + 
    info.instruction + "'");
}

この手順は任意ですが、ミューテーション分析での問題について徹底的に調査するときに確かに役立ちます。考慮すべき点としては、このような診断情報を表示するかどうかを制御する冗長スイッチを分析プログラムに追加することです。

ページのトップへ


4. 主要なミューテーション ループ

この時点で、ミューテーション分析プログラムにおいて、すべての準備作業が終わりました。もともとの、ミューテーションが発生していない、テストが行われる DLL をテスト ハーネスの実行フォルダにコピーしました。もともとの DLL に対してテスト ハーネスを実行し、それらの基準となるテスト結果を取得しました。テストが行われる DLL の IL コード イメージのインメモリのデータ ストア (listLibUnderTestOrig) を作成しました。そして、可能性のあるミューテーション、格納されている行番号、命令、およびメモリに listInfo として含んでいるメソッド名といった分岐命令に対する IL コードのイメージを分析しました。主要なミューテーション ループの高レベルの概要については、図 7 を参照してください。

図 7 主要なミューテーション ループの概要
図 7 主要なミューテーション ループの概要

ここで、テストされた DLL のインメモリ版のミューテーションが発生した IL コードを作成し、ミュータントをビルドし、そしてミュータントに対してテスト ハーネスを実行するという主要なミューテーション ループに来ました。テストが行われる、もともとの DLL にあるすべての分岐命令をミューテーションさせるには、listInfo オブジェクトを繰り返します。

for (int i = 0; i < listInfo.Count; ++i)

このループ内で、まずミューテーションが発生していない IL コードのコピーを作成します。

listLibUnderTestMutated = (ArrayList)listLibUnderTestOrig.Clone();
Info info = (Info)listInfo[i];
int lineNum = info.lineNum;
string instruction = info.instruction;

listLibUnderTestMutated という名前の ArrayList を作成し、そこに listLibUnderTestOrig のすべての行をコピーします。次に、ミューテーション可能な分岐命令を含む 1 つの行を取得し、その命令と、以前に説明した mutatedInstruction ヘルパを使用して Boolean 補数とを置き換え、もともとの行とミューテーションが発生した命令とを置き換えます。

string originalLine = (string)listLibUnderTestOrig[lineNum];
string mutatedLine = originalLine.Replace(instruction,
    mutatedInstruction(instruction));
listLibUnderTestMutated[lineNum] = mutatedLine;

次に、listLibUnderTestMutated 内の情報を外部テキスト ファイルに書き出します。

using(StreamWriter sw = 
    new StreamWriter("..\\..\\LibUnderTestMutated.il"))
for (int j = 0; j < listLibUnderTestMutated.Count; ++j)
{
    sw.WriteLine((string)listLibUnderTestMutated[j]);
}
}

この新しいファイルをどこに保存するかを確認すること以外には、慎重な対処が必要な箇所はありません。このファイルに LibUnderTestMutated.il という名前を付け、ルートのミューテーション分析プログラム フォルダに保存しました。次に、ここに示すように、ilasm.exe プログラムを使用してミューテーションが発生した DLL をミューテーションが発生した IL コードからビルドします。

using(Process assemble = Process.Start(
  "ilasm.exe", "..\\..\\LibUnderTestMutated.il " +
  "/OUTPUT=..\\..\\..\\TestHarness\\bin\\Debug\\LibUnderTest.dll /DLL"))
{
    assembly.WaitForExit();
}

ここで、.NET Framework の別の大きな利点が役に立ちます。ソース コードを再コンパイルせずに、IL コードをアセンブルすることができるので、たいていはより素早く簡単です。ilasm.exe 中間言語アセンブラ プログラムは、.NET Framework SDK とともに出荷されます。/OUTPUT スイッチを指定して、結果として作成される DLL がどこに保存されるかを制御して、テスト ハーネス実行フォルダの直下に置きます。既定の出力は .exe ファイルのため、/DLL スイッチが必要です。最後に、ミューテーション DLL の名前を、新しい名前ではなく LibUnderTest.dll にします。これは、テスト ハーネスがファイル名でバインドされ、もともとの DLL 名を探すためです。

これで、ミューテーションが発生した DLL が作成され、テスト ハーネスをこのミュータントに対して実行することができます。

using(Process test = Process.Start(
  "..\\..\\..\\TestHarness\\bin\\Debug\\TestHarness.exe")) 
{
    test.WaitForExit();
}

特別なことをする必要は何もないことに注意してください。テスト ハーネスが、テストを実行する DLL に対して名前でバインドされているため、テスト ハーネスが、自身を実行するときに新しいミュータント DLL を使用します。テストの承知が実行されると、既に述べたようなテスト スイートの結果を取得することができます。

ここで、ミューテーションの処理全体の主要な概念が出てきます。ミューテーションが発生した、テストが行われる DLL に対して実行するときに、テスト スイートがテスト ケースの失敗を引き起こしたかどうかを確認します。

Console.WriteLine(
  "Mutated number pass, fail = " + mutatedPass + " " + mutatedFail);
if (mutatedPass == basePass)
{
  Console.WriteLine(
    "\n** ALERT: Mutation did not creates failure(s) **");
  Console.WriteLine(
    "** Likely that test suite misses this code path **");
}

もともとのテスト スイートが 100% のパス率であれば、結果を解釈するのはもっと簡単です。これが本当であれば、いかなる失敗もミューテーションに起因するはずです。ですが、もしもともとの DLL にテストの失敗があれば、ミュータントに対するテスト結果が意味することに確証が持てません。テスト ケースの失敗を取得する場合は、パスするテスト ケースのみを含むサブセット テスト スイートを作成して、そのサブセットをミューテーション分析に使用すべきです。

結果の概要を追跡するために、パスしたテスト ケースおよび失敗したテスト ケースの累積数および 1 つあるいは複数のテスト ケースの失敗を引き起こしたミューテーションの数を記録します。

cumPass += mutatedPass;
cumFail += mutatedFail;

if (mutatedFail > baseFail)
  ++numMutationsWhichIncreaseFailures;

これらの値の解釈方法は、次のセクションで検討します。システムをすべての可能性のあるミューテーションに対して実行するのは、実現可能ではない場合があると述べました。この場合、主要なループで、listInfo コレクション内のすべてのオブジェクトについて繰り返すのではなく、作成したいミューテーションの数を決定して、ランダムなミューテーションを作成および選択します。コードは次のようになります。

int numMutations = 100;
Random r = new Random(0);
for (int i = 0; I < numMutations; ++i)
{
  int whichMutant = r.Next(listInfo.Count);
  // など

再現可能な結果を得るために、Random オブジェクトを作成して、シードを指定します。たとえば、Random.Next(5) を呼び出すと、0 と 4 を含めたその間の整数を返します。ですので、r.Next(listInfo.Count) を呼び出すと、Info オブジェクトの 1 つを指し示すランダムなインデックスを返します。ロジックを少し追加して、同じミューテーションを 2 度テストしないようにすることを考慮するかもしれません。

ページのトップへ


5. ミューテーション分析の結果

ミューテーション分析の最後の部分は、結果の表示および解釈です。これが、図 1 の出力を生じさせるコードです。

double alphaPower = (
  numMutationsWhichIncreaseFailures * 1.0) / listInfo.Count;
double betaPower = (cumFail * 1.0) / (cumFail + cumPass);

ミューテーション分析の結果を使用する 3 つの方法があります。1 番目は、テスト ケースの失敗を作成しないミュータントがある場合はいつでも、テスト スイートがミュータントを含むコード パスをテストしていないと思われます。これは、図 1 で示されました。調査および新しいテスト ケースの作成が必要です。

ミューテーション結果を使用する 2 番目の方法は、ミューテーション テストに関連してテスト スイートの有用性の尺度として使用するというものです。これを、テスト スイート関連アルファ能力と呼びます。これは、失敗を作成するミュータントの数と、ミュータントの総数との比率です。たとえば、ミューテーションを 100 個作成して、それらのうち 90 個がテスト ケースの失敗を生み出した場合、アルファ能力は 90÷100 = 0.90 となります。アルファは 0 と 1 の間であり、大きい数字であればあるほど良い数字です。製品開発中に長期間にわたってテスト ケースをテスト スイートに追加するときにアルファ能力を追跡すると、テスト スイートの有用性の向上を測定することができます。

ミューテーション結果を使用する 3 番目の方法は、ミュータントにより生じるテスト ケースの失敗数全体と、テスト ケースの合計数を比較することです。このテスト スイート ミューテーション ベータ能力は、テスト スイート アルファ能力と相互に深く関連しています。1,000 のテスト ケースを含むテスト スイートがあるとします。20 のミュータントに対して実行し、合計で 80 のテスト ケースの失敗が生じます。ベータ能力は次のようになります。

80 / (1,000 * 20) = 0.004

失敗を作成すればするほど、ベータ能力の数値は大きくなります。そのため、ベータ能力も 0 と 1 の間の数値であり、大きい数字であればあるほどより有効なテスト スイートであることを示します。ベータ能力は、テスト スイートの有用性の指標としてはアルファ能力よりも重要ではありませんが、それでもアルファ能力の数的指標を評価する方法として役立ちます。

ページのトップへ


6. まとめ

用語 "ミューテーション テスト" は、少し誤った名前です。この技術は、ミューテーション分析と呼ぶほうが良いかもしれません。なぜなら、その結果はパスした結果あるいは失敗した結果というよりはむしろ、テスト スイートの有用性の尺度であるためです。ミューテーション テスト/分析は、コード カバレッジ分析の親しいいとこのようなものです。コード カバレッジは一般的に、アンマネージ開発環境においてはミューテーション テストよりも簡単です。

コード カバレッジにより、テストが行われるコードのどの部分 (これはメソッド、ブロック、あるいは個々のステートメントを指します) が、テスト スイートの作用を受けるのかが分かります。そして、ミューテーション テストは同様の結果を得るための 1 つの方法です。この記事で示したシステムの配置が簡単であるため、ミューテーション分析がコード カバレッジと同様に簡単になってしまったかもしれません。Visual Studio 2005 ではコード カバレッジを包括的にサポートしていますが、いくつかのミューテーション分析ツールとともに、ここで検討したすべての方法をテスト スイートに統合することができます。

ページのトップへ


James McCaffrey は、Volt Information Sciences Inc. に勤務しており、マイクロソフトで働くソフトウェア エンジニア向けの技術トレーニングを管理しています。Internet Explorer および MSN Search を含むいくつかのマイクロソフト製品に取り組んでいます。連絡先は、jmccaffrey@volt.com あるいは v-jammc@microsoft.com です。


この記事は、MSDN マガジン - 2006 年 4 月からの翻訳です。

QJ: 060403

ページのトップへ