ごく単純な変異テスト
James McCaffrey
私が知っているテスト担当者の多くは変異テストについて耳にしたことがあっても、実際に実行した人はほとんどいません。変異テストは、難しいうえに、高価なサードパーティ製のソフトウェア ツールが必要だと考えられています。しかし、今月は、C# と Visual Studio を使用して、ごく単純な (2 ページ未満のコードと 4 時間未満の作業で作成できる) 変異テスト システムを作成する方法について説明します。変異テスト システムを単純に保っておくと、わずかな時間と労力で本格的な変異システムのメリットの大半を享受できます。
変異テストとは、一連のテスト ケースの効果を測定する手段です。考え方はシンプルです。テスト ケースが 100 とおりあり、テスト対象のシステム (SUT: System Under Test) がこの 100 とおりのテスト ケースすべてに合格するとします。SUT を変異させる (">" を "<" に変えたり "+" を "-" に変えたりする) と、この SUT ではバグが発生すると推定されます。ここで、100 とおりのテスト ケースを再実行すると、少なくとも 1 つのテスト ケースで問題のあるコードが検出されたことが示され、不合格になると予想されます。しかし、どのテストも不合格にならなければ、用意したテスト ケースでは問題のあるコードを検出できず、SUT を完全にはテストできていない可能性が非常に高くなります。
今回の目的は、図 1 をご覧いただければ一目瞭然です。
図 1 変異テストのデモの実行
この例の SUT は、MathLib.dll というライブラリです。ここで紹介する技法は、DLL、WinForms アプリケーション、ASP.NET Web アプリケーションなど、ほとんどの Microsoft .NET Framework システムのテストに使用できます。変異システムでは、まず SUT の元のソース コードをスキャンして、変異させる候補となるコードを探します。ここではごく単純なシステムにし、"<" 演算子と ">" 演算子だけを探します。テスト システムは、2 つの変異形を作成して評価するよう設定します。運用シナリオでは、多くの場合、数百または数千の変異形を作成することになります。最初の変異形は、変異させる演算子をランダムに選択し (この例では、SUT ソース コードの文字位置 189 にある ">" 演算子)、その演算子を "<" に変異させます。次に、変異させた DLL のソース コードをビルドして、変異させた MathLb.dll ライブラリを作成します。続いて、変異させた SUT に対してテスト ケースのスイートを呼び出し、結果をファイルにログ記録します。2 回目の反復処理でも、同じ方法で 2 つ目の変異形を作成してテストします。ログ ファイルの結果は次のとおりです。
=============
Number failures = 0
Number test case failures = 0 indicates possible weak test suite!
=============
Number failures = 3
This is good.
=============
最初の変異形では不合格となったテスト ケースがありませんでした。つまり、位置 189 のソース コードを調べて、いずれのテスト ケースでもそのコードがテストされなかった理由を突き止める必要があります。
SUT
このごく単純な変異テストのデモは、3 つの Visual Studio プロジェクトから構成されています。1 つ目のプロジェクトは SUT を含み、このデモでは MathLib という C# クラス ライブラリです。2 つ目のプロジェクトはテスト ハーネスの実行可能ファイルで、このデモでは TestMutation という C# コンソール アプリケーションです。3 つ目のプロジェクトは変異形を作成してビルドします。このデモでは Mutation という C# コンソール アプリケーションです。利便性のため、3 つのプロジェクトすべてを MutationTesting という 1 つのディレクトリに配置しました。変異テストでは、追跡対象のファイルやフォルダーが多数になるため、ファイルやフォルダーを整理する手間を軽視しないでください。このデモでは、Visual Studio 2008 を使用して (ただし、任意のバージョンの Visual Studio で動作します) ダミーの MathLib クラス ライブラリを作成しています。ダミーの SUT の完全なソース コードを図 2 に示します。
図 2 ダミー SUT の完全なソース コード
using System;
namespace MathLib
{
public class Class1
{
public static double TriMin(double x, double y, double z)
{
if (x < y)
return x;
else if (z > y)
return y;
else
return z;
}
}
}
既定のクラス名 Class1 をそのまま使用しています。このクラスは、3 つの double 型パラメーターの最小値を返す、TriMin という静的メソッドを 1 つ含みます。また、この SUT では処理をわざと間違えていることに注意してください。たとえば、x = 2.0、y = 3.0、z = 1.0 の場合、TriMin メソッドは正しい 1.0 ではなく 2.0 を返します。しかし、重要なのは、変異テストは SUT の正確性を直接測定するのではなく、一連のテスト ケースの効果を測定することです。SUT をビルドしたら、次はソース ファイル Class1.cs のベースラインとなるコピーを変異テスト システムのルート ディレクトリに保存します。各変異形が SUT の元のソース コードを 1 か所変更するという考え方から、元の SUT ソースのコピーを保持しておく必要があります。この例では、元のソースを Class1-Original.cs という名前で C:\MutationTesting\Mutation に保存しました。
テスト ハーネス
テスト状況によっては、テスト ケース データの既存セットがあることも、既存のテスト ハーネスが存在することもあります。ここでは今回のごく単純な変異テスト用に、TestMutation という C# コンソール アプリケーション テスト ハーネスを作成します。Visual Studio でプロジェクトを作成したら、SUT (C:\MutationTesting\MathLib\bin\Debug にある MathLib.dll) への参照を追加します。テスト ハーネス プロジェクトの完全なソース コードを図 3 に示します。
図 3 テスト ハーネスとテスト データ
using System;
using System.IO;
namespace TestMutation
{
class Program
{
static void Main(string[] args)
{
string[] testCaseData = new string[]
{ "1.0, 2.0, 3.0, 1.0",
"4.0, 5.0, 6.0, 4.0",
"7.0, 8.0, 9.0, 7.0"};
int numFail = 0;
for (int i = 0; i < testCaseData.Length; ++i) {
string[] tokens = testCaseData[i].Split(',');
double x = double.Parse(tokens[0]);
double y = double.Parse(tokens[1]);
double z = double.Parse(tokens[2]);
double expected = double.Parse(tokens[3]);
double actual = MathLib.Class1.TriMin(x, y, z);
if (actual != expected) ++numFail;
}
FileStream ofs = new FileStream("..\\..\\logFile.txt",
FileMode.Append);
StreamWriter sw = new StreamWriter(ofs);
sw.WriteLine("=============");
sw.WriteLine("Number failures = " + numFail);
if (numFail == 0)
sw.WriteLine(
"Number test case failures = " +
"0 indicates possible weak test suite!");
else if (numFail > 0)
sw.WriteLine("This is good.");
sw.Close(); ofs.Close();
}
}
}
テスト ハーネスに 3 とおりのテスト ケースをハードコーディングしていることに注目してください。運用環境では、多くの場合、数百とおりのテスト ケースをテキスト ファイルに格納して、ファイル名を args[0] として Main に渡します。"1.0, 2.0, 3.0, 1.0" という最初のテスト ケースは、SUT の TriMin メソッドの、x パラメーター、Y パラメーター、および z パラメーター (1.0、2.0、および 3.0) と、想定する結果 (1.0) を表します。テスト セットが不適切なのは明らかです。3 とおりのテスト ケースはすべて基本的に同じことを意味し、最小値は x パラメーターになります。このまま元の SUT を調べれば、3 とおりのテスト ケースすべてに合格することがわかります。今回の変異テスト システムでは、このテスト セットの弱点を検出できるでしょうか。
テスト ハーネスでは各テスト ケースを反復処理し、入力パラメーターと想定する戻り値を解析します。次に、入力パラメーターを指定して SUT を呼び出し、実際の戻り値を取得し、想定する戻り値と比較して、テスト ケースの合格または不合格を判断します。最後に、テスト ケースが不合格になった合計数を集計します。変異テストでは、テスト ケースが合格した数ではなく、新たに不合格が少なくとも 1 つ生じるかどうかに着目していることを思い出してください。テスト ハーネスは、呼び出し元プログラムのルート フォルダーにログ ファイルを書き込みます。
変異テスト システム
ここでは、変異テスト プログラムを 1 行ずつ説明します。ただし、図 1 に表示されている出力の生成に使用する、WriteLine ステートメントのほとんどは省略します。ルートの MutationTesting ディレクトリに Mutation という C# コンソール アプリケーションを作成しています。プログラムの冒頭は次のとおりです。
using System;
using System.Collections.Generic;
using System.IO;
using System.Diagnostics;
using System.Threading;
namespace Mutation
{
class Program
{
static Random ran = new Random(2);
static void Main(string[] args)
{
try
{
Console.WriteLine("\nBegin super-simple mutation testing demo\n");
...
Random オブジェクトの目的は、ランダムな変異位置を生成することです。ここではシード値に 2 を使用しましたが、任意の値を使用できます。次に、ファイルの場所を設定します。
string originalSourceFile = "..\\..\\Class1-Original.cs";
string mutatedSourceFile = "..\\..\\..\\MathLib\\Class1.cs";
string mutantProject = "..\\..\\..\\MathLib\\MathLib.csproj";
string testProject = "..\\..\\..\\TestMutation\\TestMutation.csproj";
string testExecutable =
"..\\..\\..\\TestMutation\\bin\\Debug\\TestMutation.exe";
string devenv =
"C:\\Program Files (x86)\\Microsoft Visual Studio 9.0\\Common7\\IDE\\
devenv.exe";
...
これらの各ファイルの使用方法については、後で説明します。Visual Studio 2008 に関連付けられている devenv.exe プログラムを指していることに注意してください。この場所をハードコーディングする代わりに、devenv.exe のコピーを作成して変異システムのルート フォルダーに配置することもできます。
プログラムは次のようにの続きます。
List<int> positions = GetMutationPositions(originalSourceFile);
int numberMutants = 2;
...
GetMutationPositions ヘルパー メソッドを呼び出して元のソース コード ファイルをスキャンし、すべての "<" 文字と ">" 文字の文字位置を List に格納し、作成してテストする変異形の数を 2 に設定します。
メインの処理ループは、次のとおりです。
for (int i = 0; i < numberMutants; ++i) {
Console.WriteLine("Mutant # " + i);
int randomPosition = positions[ran.Next(0, positions.Count)];
CreateMutantSource(originalSourceFile, randomPosition, mutatedSourceFile);
try {
BuildMutant(mutantProject, devenv);
BuildTestProject(testProject, devenv);
TestMutant(testExecutable);
}
catch {
Console.WriteLine("Invalid mutant. Aborting.");
continue;
}
}
...
ループ内では、変異させる文字のランダムな位置を変異位置候補の List から取得します。続いて、ヘルパー メソッドを呼び出して、変異させた Class1.cs ソース コードを生成し、対応する MathLib.dll をビルドし、新しい変異形を使用するようテスト ハーネスをリビルドし、変異させた DLL をテストします。テストでは、エラーが発生すると考えられます。変異させたソース コードが有効ではなくなる可能性が高いため、ビルドできないコードのテストを中止できるように、ビルドしてテストしようとする処理を try-catch ステートメントでラップしています。
Main メソッドの最後は次のとおりです。
...
Console.WriteLine("\nMutation test run complete");
}
catch (Exception ex) {
Console.WriteLine("Fatal: " + ex.Message);
}
} // Main()
変異させたソース コードの作成
変異位置候補のリストを取得するヘルパー メソッドを次に示します。
static List<int> GetMutationPositions(string originalSourceFile)
{
StreamReader sr = File.OpenText(originalSourceFile);
int ch = 0; int pos = 0;
List<int> list = new List<int>();
while ((ch = sr.Read()) != -1) {
if ((char)ch == '>' || (char)ch == '<')
list.Add(pos);
++pos;
}
sr.Close();
return list;
}
このメソッドでは、ソース コードを 1 文字ずつ調べて、大なり演算子と小なり演算子を探し、文字位置を List コレクションに追加します。ご覧のとおり、このごく単純な変異システムには、">" や "+" など 1 文字のトークンだけを変異させることができ、">=" など複数文字のトークンは変異させることができないという制限があります。SUT のソース コードを実際に変異させるヘルパー メソッドを、図 4 に示します。
図 4 CreateMutantSource メソッド
static void CreateMutantSource(string originalSourceFile,
int mutatePosition, string mutatedSourceFile)
{
FileStream ifs = new FileStream(originalSourceFile, FileMode.Open);
StreamReader sr = new StreamReader(ifs);
FileStream ofs = new FileStream(mutatedSourceFile, FileMode.Create);
StreamWriter sw = new StreamWriter(ofs);
int currPos = 0;
int currChar;
while ((currChar = sr.Read()) != -1)
{
if (currPos == mutatePosition)
{
if ((char)currChar == '<') {
sw.Write('>');
}
else if ((char)currChar == '>') {
sw.Write('<');
}
else sw.Write((char)currChar);
}
else
sw.Write((char)currChar);
++currPos;
}
sw.Close(); ofs.Close();
sr.Close(); ifs.Close();
}
CreateMutantSource メソッドは、前に保存しておいた元のソース コード ファイル、変異させる文字位置、および保存する結果の変異形ファイルの名前と場所を受け取ります。ここでは "<" 文字と ">" 文字だけを確認していますが、他の変異も検討することをお勧めします。一般に、有効なソースが生成される変異が必要なので、">" を "=" に変更することはお勧めしません。また、複数の場所で変異させることもお勧めしません。これは、1 か所変異させるだけで新しいテスト ケースが不合格となり、テスト セットが実際には不適切でも適切に見えることがあるためです。実質的な効果がない変異もあれば (コメント内の文字の変異など)、無効なコードが生成される変異もあります (">>" シフト演算子を "><" に変異させるなど)。
変異形のビルドとテスト
BuildMutant ヘルパー メソッドを次に示します。
static void BuildMutant(string mutantSolution, string devenv)
{
ProcessStartInfo psi =
new ProcessStartInfo(devenv, mutantSolution + " /rebuild");
Process p = new Process();
p.StartInfo = psi; p.Start();
while (p.HasExited == false) {
System.Threading.Thread.Sleep(400);
Console.WriteLine("Waiting for mutant build to complete . . ");
}
p.Close();
}
Process オブジェクトを使用して、devenv.exe プログラムを呼び出し、Visual Studio ソリューションをリビルドします。このソリューションには Class1.cs という変異済みソース コードが保持され、MathLib.dll 変異形を作成します。引数を渡さない場合、devenv.exe は Visual Studio IDE を起動しますが、引数を渡すと、devenv を使用してプロジェクトやソリューションをリビルドすることができます。ここでは遅延ループを使用して 400 ミリ秒ずつ一時停止し、devenv.exe で変異させた DLL のビルドを完了する時間を確保しています。この時間を確保しないと、変異システムでは、変異させた SUT が作成される前にその SUT のテストを試みる可能性があります。
テスト ハーネスをリビルドするヘルパー メソッドを次に示します。
static void BuildTestProject(string testProject, string devenv)
{
ProcessStartInfo psi =
new ProcessStartInfo(devenv, testProject + " /rebuild");
Process p = new Process();
p.StartInfo = psi; p.Start();
while (p.HasExited == false) {
System.Threading.Thread.Sleep(500);
Console.WriteLine("Waiting for test project build to complete . . ");
}
p.Close();
}
このメソッドの主な目的は、テスト プロジェクトをリビルドすることで、テスト ハーネスの実行時に、以前に使用した変異形 SUT ではなく新しい変異形 SUT を使用することです。変異させたソース コードが無効であれば、BuildTestProject メソッドは例外をスローします。
ごく単純な変異テスト システムの最後の部分は、テスト ハーネスを呼び出すヘルパー メソッドです。
...
static void TestMutant(string testExecutable)
{
ProcessStartInfo psi = new ProcessStartInfo(testExecutable);
Process p = new Process(); p.StartInfo = psi;
p.Start();
while (p.HasExited == false)
System.Threading.Thread.Sleep(200);
p.Close();
}
} // class Program
} // ns Mutation
既に説明したように、テスト ハーネスではハードコーディングしたログ ファイルの名前と場所を使用します。この情報をパラメーターとして TestMutant メソッドに渡し、Process の開始情報内 (TestMutation.exe テスト ハーネスで受け取る場所) に配置すると、ファイルの情報をパラメーター化できます。
実際に使用できる変異テスト システム
変異テストの原則は単純ですが、本格的な変異テスト システムを作成する作業の細部は困難です。しかし、変異システムをできる限り単純に保ち、Visual Studio と devenv.exe を使用すると、驚くほど効果的な .NET SUT 用の変異テスト システムを作成できます。ここで説明した例を使用すれば、ユーザー独自の SUT 用の変異テスト システムを作成できます。サンプルの変異テスト システムの主な制限は、1 文字の変更に基づいているため、複数文字の演算子を簡単には変異させることができない点です (">=" 演算子をその対極を意味する "<" に変更するなど)。もう 1 つの制限は、変異させる文字位置だけがシステムから通知されるため、変異形を簡単に診断できないことです。このような制限はありますが、ここで作成したサンプル システムは、いくつもの中規模ソフトウェア システムでテスト スイートの効果を測定するのに役立っています。
Dr. James McCaffrey は Volt Information Sciences Inc. に勤務し、ワシントン州レドモンドにあるマイクロソフト本社で働くソフトウェア エンジニアの技術トレーニングを管理しています。これまでに、Internet Explorer、MSN サーチなどの複数のマイクロソフト製品にも携わってきました。また、『.NET Test Automation Recipes: A Problem-Solution Approach』(Apress、2006 年) の著者でもあります。連絡先は jammc@microsoft.com (英語のみ) です。
この記事のレビューに協力してくれた技術スタッフの Paul Koch、Dan Liebling、および Shane Williams に心より感謝いたします。