本文章是由機器翻譯。

測試回合

超簡單突變測試

詹姆斯 McCaffrey

下載程式碼範例

我知道大部分軟體測試人員已經聽過的變化測試,但只有少數確實執行它。變化測試中有困難且需要昂貴的協力廠商軟體工具的聲望。不過,在本月的專欄中,我將告訴您如何建立測試系統使用 C# 和 Visual Studio super-simple (程式碼,並 4 小時的時間少於兩個頁面) 變化。保留簡單變化測試系統,您可以在一小部份的時間與精力取得大部分的成熟變化系統的優點。

變化測試是用來測量的一組測試案例的效能。概念是很簡單。假設您開始 100 測試情況下,您的系統測試 (SUT) 可通過所有的 100 測試。如果您改變SUT — 比方說,藉由變更">"到"<",或變更一個"+"來"-"— 我們可以假設引入 SUT 的錯誤。現在如果您重新執行您 100 的測試案例,您所預期至少一個表示其中一種測試案例的測試案例失敗攔截到錯誤的程式碼。但如果您不看到任何測試失敗,則它很可能是您的測試案例集遺漏程式碼,並沒有徹底操作 SUT。

您可以看到我在此對話方塊向的最佳方法是藉由查看圖 1

圖 1測試示範執行的變化

在這個範例中 SUT 是名為 MathLib.dll 程式庫。我在此處呈現的技巧可以用來測試大部分 Microsoft。NET Framework 系統包括 WinForms 應用程式,ASP 的 Dll。NET Web 應用程式和等等。變化系統開始 SUT,尋找候選程式碼來改變掃描原始來源程式碼。我 super-simple 的系統看起來只適用於"<"和">"運算子。測試系統設定來建立並評估兩個 mutants。在實際執行案例中,您可能會建立數百或甚至上千個 mutants。第一個 mutant 隨機選取運算子來改變,在此情況下">"字元運算子 189 置於 SUT 的原始程式碼,和 mutates 要的語彙基元"<"。接下來,衍生 DLL 原始程式碼是為了建立衍生的 MathLb.dll 程式庫。然後變化系統 mutant SUT,到檔案的記錄結果上呼叫的測試案例的套件。第二個反覆項目會建立,並以相同的方式會測試第二個 mutant。記錄檔的結果是:

=============
數字失敗 = 0
數字的測試案例失敗 = 0 表示可能弱測試套件!
=============
數字失敗 = 3
這是很好。
=============

第一個 mutant 沒有產生任何測試案例失敗,這表示您應該檢查位置 189 原始程式碼,並判斷為何沒有任何測試案例執行該程式碼。

SUT

測試示範我 super-simple 變化是由三個 Visual Studio 專案所組成。第一個專案保有 SUT,而且在此情況下是 C# 類別庫名為 MathLib。第二個專案是測試控管可執行檔,在此情況下的 C# 主控台應用程式名稱為 TestMutation。第三個專案會建立並建置 mutants,在此案例中的 C# 主控台應用程式命名變化。為了方便起見我需要將所有的三個專案置於名為 MutationTesting 的單一目錄。與變化測試有很多要追蹤的檔案及資料夾,您不應該低估保持這些組織的挑戰。這個示範我使用 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 的預設類別名稱。 類別包含單一的靜態方法,TriMin,它會傳回三個型別雙精度浮點數參數的最小值。 也請注意 SUT 是故意不正確。 例如,如果 x = 2.0 中,y = 3.0 和 z = 1.0,TriMin 方法會傳回 2.0 版本,而不是正確的 1.0 值。 不過,很重要的一點變化測試並不會直接測量正確性的 SUT; 變化測試會測量的測試案例組的效率。 在建置之後 SUT 下, 一步是將原始程式檔,Class1.cs,比較基準複本儲存到變化測試系統的根目錄位置。 其概念是每個 mutant 是原始來源程式碼的 SUT 中的單一所做的修改,所以必須維護一份原始的 SUT 來源。 在這個範例中我儲存的原始來源為 Class1 Original.cs C:\MutationTesting\Mutation 目錄中。

測試控管

在某些測試的情況下,您可能會有現有的測試案例資料集,在某些情況下,您有現有的測試控管。 對於測試系統此 super-simple 變化,我建立 C# 主控台應用程式測試控管名為 TestMutation。 建立專案之後在 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();
    }
  }
}

觀察測試控管有三個硬式編碼測試案例。 在實際執行環境中,您可能會有許多數百名儲存在文字檔中的測試案例並將您無法傳遞的檔名至主要為 args [0]。 第一個測試案例中,1.0、 2.0、 3.0、 1.0,代表 x、 y 和 z 參數 (1.0、 2.0 和 3.0),SUT 的 TriMin 方法後面的預期結果 (1.0)。 很明顯測試組不足:每三個測試案例基本上對等,做為 x 參數的最小值。 但是,如果您檢查原始 SUT,就會發現事實上會傳遞所有三個測試案例。 將我們變化的測試系統偵測到測試組的弱點嗎?

測試控管會逐一查看每個測試案例,剖析出的輸入的參數和傳回值,會呼叫 SUT 以輸入參數、 擷取實際傳回值、 比較實際傳回與預期返回判斷測試案例通過/失敗結果,並再往上累加測試案例失敗總數。 回想一下在測試變化,我們很主要是否至少一個新的失敗,而不是多少測試情況下傳遞。 測試控管會寫入呼叫程式的根資料夾中的記錄檔。

變化測試系統

在本節中,我將引導您完成一次測試程式一行變化,但省略大部分 WriteLine 陳述式用來產生輸出所示的圖 1。 我建立 C# 主控台應用程式在根 MutationTesting 目錄中命名的變化。 程式開頭:

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");
...

隨機物件的目的是要產生隨機變化位置。 我使用識別值種子值為 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;
...

我稱之為審視原來的原始程式碼檔案並儲存所有的字元位置的 helper GetMutationPositions 方法"<"和">"字元來建立和進行測試,以兩個清單中,以及組 mutants 的數目。

主要處理迴圈是:

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;
  }
}
...

在這個迴圈中,程式會擷取要改變的可能位置清單中字元的任意位置,並接著會呼叫 helper 方法,若要產生衍生 Class1.cs 原始程式碼、 建置相對應的 mutant MathLib.dll、 重建測試控管,讓它使用新的 mutant 和再測試 mutant DLL,可以即時趕到產生錯誤。 因為它是很有可能突變的原始程式碼可能無法有效,我換行,若要建置和測試 try catch 陳述式中,因此我可以中止測試無法建置程式碼嘗試。

Main 方法共同構成為:

...
Console.WriteLine("\nMutation test run complete");
  }
  catch (Exception ex) {
    Console.WriteLine("Fatal: " + ex.Message);
  }
} // Main()

建立 Mutant 的原始程式碼

要取得一份可能變化位置的 helper 方法是:

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;
}

此方法尋找大於一次走到來源的程式碼的一個字元位與小於-比運算子,並將字元位置加入至清單集合。 請注意表示這個 super-simple 的變化系統的限制是它可以只變更單一字元語彙基元例如">"或"+"並不能如處理的 multicharacter 語彙基元"> ="。 若要實際改變 SUT 來源程式碼 helper 方法會列在圖 4

圖 4CreateMutantSource 方法

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 方法會接受原始的來源原始程式碼檔中所儲存的比較離開更早版本,以及與要改變的字元位置以及名稱和產生的衍生檔案儲存至的位置。 在此我只是檢查"<"和">"字元,但您可能要考慮其他變化。 在一般情況下,您想要將產生的變化有效來源,因此,比方說,您不會變更">""="。 同樣地,變更在一個以上的位置不是個好主意,因為其中一則變化可能會產生新的測試案例失敗,建議測試集很好的事實上它可能不是。 某些變化會有沒有實際的效果 (例如變更註解內的字元),而某些變化會產生無效的程式碼 (例如變更">>"移位運算子以"><")。

建置和測試 Mutant

BuildMutant helper 方法是:

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();
}

我要叫用來重建 Visual Studio 方案房屋 Class1.cs 更動原始碼,會產生 MathLib.dll mutant devenv.exe 程式使用處理序物件。 不需引數,devenv.exe 會啟動 Visual Studio IDE,但是當傳遞引數,devenv 可以用來重建的專案或方案。 請注意我使用 delay 迴圈,暫停每 400 毫秒,以提供 devenv.exe 的時間才能完成建置 mutant DLL; 否則變化系統可能會嘗試之前就會建立測試 mutant SUT。

若要重新建置測試控管的 helper 方法是:

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();
}

主要的概念是,重建測試專案中,新測試控管執行而不是先前使用 mutant SUT 時,將會使用 SUT 的 mutant。 如果您的衍生的來源程式碼無效,BuildTestProject 將會擲回例外狀況。

測試系統 super-simple 變化的最後一個部分是叫用測試控管的 helper 方法:

...
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,並將它放置在程序內啟動資訊],它會接受 TestMutation.exe 測試控管。

真實世界,使用變化測試系統

變化測試的原理很簡單,但建立成熟變化測試系統的詳細資料是一大挑戰。不過,只要將變化系統保存可能和運用 Visual Studio 和 devenv.exe 一樣簡單,您可以建立測試的系統是令人意外有效變化。NET SUTs。使用我在此處提供的範例,您應該能夠建立您自己的 SUTs 測試系統變化。測試系統範例變化的主要限制是因為系統以單一字元變更為基礎,您無法輕易地執行 multicharacter 運算子,例如變更變化"> ="至其"<"補數運算子。另一個限制是變化的系統僅提供您的字元位置,因此它並不會提供您簡便的方式來診斷 mutant。不論這些限制,我的範例系統已經使用成功以測量測試套件的數個中型軟體系統的效能。

Dr. James McCaffrey 詹姆斯 McCaffrey適用於 Volt 資訊科學 Inc.,他用來管理技術訓練軟體工程師使用 Microsoft 里德蒙,Wash.,在校園。他工作了幾個微軟產品,包括互聯網資源管理器和 MSN 搜索。 博士。 麥卡弗裡是作者"。網路測試自動化食譜"(Apress,2006年),可以在達到jammc@microsoft.com

除了感謝下列 Microsoft 技術專家,來檢閱這份文件:Paul Koch, Dan LieblingShane Williams