我認識的很多測試人員都聽說過變化測試,卻很少有人執行過該測試。 變化測試以難度大、需要昂貴的第三方軟件工具而聞名。 但是,在本月專欄中我將爲您展示如何用 C# 和 Visual Studio 創建一個超簡單(不到 2 頁代碼,用不了 4 個小時)的變化測試系統。 簡單的變化測試系統能讓您用很少的時間和精力,就獲得成熟的變化測試系統所提供的大部分好處。
變化測試是一種評估一組測試用例有效性的方法。 其原理很簡單。 假設您有 100 個測試用例,並且被測系統 (SUT) 通過了這 100 個測試。 如果您改變 SUT,例如把“>”更改爲“<”或把“+”更改爲“-”,則有可能將一個錯誤引入到了 SUT。 現在,如果您重新運行這 100 個測試用例,則可以預見至少有一個測試用例會失敗,表明其中一個測試用例捕獲了錯誤代碼。 但是,如果未看到測試失敗,則很可能是您的這組測試用例錯過了錯誤代碼,未能徹底測試 SUT。
若要了解我將要介紹的內容,最好是看一下圖 1。
圖 1 變化測試運行演示
該示例中的 SUT 是一個名爲 MathLib.dll 的庫。 我在這裏介紹的方法可用於測試大部分的 Microsoft .NET Framework 系統,包括 DLL、WinForms 應用程序、ASP.NET Web 應用程序等。 變化系統首先掃描 SUT 的原始源代碼,尋找可進行更改的候選代碼。 我的超簡單系統只搜索“<”和“>”運算符。 測試系統設置爲創建並評估兩處變化。 在實際的工作中,您可能會創建上百甚至上千個變化。 第一處變化會隨機選擇並更改一個運算符,本示例中,是 SUT 源代碼中字符位置 189 上的“>”運算符,並將此符號更改爲“<”。 接下來,構建變化 DLL 源代碼以創建變化的 MathLb.dll 庫。 然後,變化系統調用變化的 SUT 上的一組測試用例,並將結果記錄到文件中。第二次迭代會以同樣的方法創建和測試第二處變化。 日誌文件的結果如下所示:
=============
Number failures = 0
Number test case failures = 0 indicates possible weak test suite!
=============
Number failures = 3
This is good.
=============
第一個變化未生成任何測試用例失敗,這說明您應該檢查 189 位置上的源代碼並確定爲何沒有測試用例檢查該代碼。
SUT
我的超簡單變化測試演示包含 3 個 Visual Studio 項目。 第一個項目包含 SUT,在本例中是一個名爲 MathLib 的 C# 類庫。 第二個項目是一個可執行的測試工具,在本例中是一個名爲 TestMutation 的 C# 控制檯應用程序。 第三個項目創建和構建變化,在本例中是一個名爲 Mutation 的 C# 控制檯應用程序。 爲方便起見,我將三個項目放置到一個名爲 MutationTesting 的目錄中。 在變化測試中要跟蹤很多文件和文件夾,您不應忽視妥善組織它們所面臨的難度。 對於此演示,我使用了 Visual Studio 2008(可使用任何版本的 Visual Studio)來創建虛擬的 MathLib 類庫。 圖 2 顯示了該虛擬 SUT 的完整源代碼。
圖 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,它返回三個 Double 參數中最小的一個。 還要注意,該 SUT 有意設計的不正確。 例如:如果 x = 2.0、y = 3.0 並且 z = 1.0,該 TriMin 方法會返回值 2.0 而不是正確值 1.0。 但是,請務必注意,變化測試並不 直接衡量 SUT 的正確性,而是衡量一組測試用例的有效性。 構建 SUT 後,下一步就是將源文件 Class1.cs 的一個基準副本保存到變化測試系統的根目錄中。之所以這麼做是因爲,每次變化都會對 SUT 的原始源代碼進行一次修改,因此必須保留一份 SUT 原始源代碼。在本示例中,我將原始源代碼保存在 C:\MutationTesting\Mutation 中,名爲 Class1-Original.cs。
測試工具
在某些測試中,您可能會有一組現成的測試用例數據,在另一些測試中,您可能會有現成的測試工具。 對於這個超簡單變化測試系統,我創建了一個名爲 TestMutation 的 C# 控制檯應用程序測試工具。 在 Visual Studio 中創建該項目後,我添加一個對 SUT 的引用。MathLib.dll 位於 C:\MutationTesting\MathLib\bin\Debug。 圖 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] 傳遞到 Main 中。 在第一個測試用例中,“1.0, 2.0, 3.0, 1.0,”代表 x、y 和 z 參數(1.0、2.0 和 3.0),後面是 SUT 的 TriMin 方法的預期結果 (1.0)。 很明顯,測試集是不充分的:這三個測試用例中的每一個基本上都是等效的,並且將最小值作爲 x 參數。 但是,如果您研究原始的 SUT,您會發現事實上三個測試用例都會通過。 我們的變化測試系統能夠檢測出這個測試集的缺陷嗎?
測試工具迭代每個測試用例,分析輸入參數和預期返回值,使用輸入參數調用 SUT,提取實際返回值,將實際返回值和預期返回值進行比較,從而確定測試用例結果爲通過還是失敗,然後累計測試用例失敗的總數量。 請注意,在變化測試中,我們主要關注是否至少有一個新的測試用例失敗,而不是有多少測試用例通過。 測試工具將日誌文件寫入到調用程序的根文件夾中。
變化測試系統
在這部分中,我將逐行向您介紹變化測試程序,但是省略了圖 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";
...
稍後,您將看到如何使用每個文件。 請注意,我所指向的 devenv.exe 程序與 Visual Studio 2008 相關聯。 我沒有對該位置進行硬編碼,而是生成一個 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;
- }
- }
- ...
在循環中,程序會提取字符的隨機位置以便從可能位置列表進行變化,然後調用幫助程序方法來生成變化的 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;
- }
該方法逐個字符匹配源代碼,尋找大於和小於運算符,並將字符位置添加到 List 集合中。 請注意,這裏介紹的超簡單變化測試系統存在一項限制,即只能變化單字符的符號,如“>”或“+”,而不能變化多字符的符號,如“>=”。實際用於變化 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 方法接受原始的源代碼文件(已提前保存),還接受要變化的字符位置和生成的變化文件的名稱及保存位置。 此處,我僅檢查“<”和“>”字符,但您可能希望看一下其他變化。 通常,您希望變化會產生有效的源,因此您不能將“>”改成“=”。 此外,在多個位置上變化也是不可取的,因爲這多個變化中可能只有一個變化會生成新的測試用例失敗,從而表明測試集有效,但其實並非如此。 一些變化沒有實用性(例如變化註釋裏的字符),另一些變化會產生無效代碼(例如將移位運算符“>>””更改爲“><“)。
構建和測試變化
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 創建之前即嘗試測試它。
用於重新構建測試工具的幫助程序方法是:
- 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 的 StartInfo 中(在此,它可被 TestMutation.exe 測試工具接受)。
實際應用的變化測試系統
變化測試從原理上講並不複雜,但是創建一個成熟的變化測試系統需要注意的細節卻很有挑戰性。 然而,通過儘可能簡化變化測試系統,並利用 Visual Studio 和 devenv.exe,您可針對 .NET SUT 創建非常有效的變化測試系統。 使用我在此處介紹的示例,您應該可以創建自己的 SUT 變化測試系統。 示例變化測試系統的主要限制在於,由於該系統是基於單字符更改,因此,您不能輕鬆執行多字符運算符的變化,例如將“>=”更改爲其求補運算符“<”。 另一個限制是,該系統僅爲您提供變化的字符位置,不能讓您輕鬆診斷變化。 儘管存在這些限制,但該示例系統已成功運用到許多中型軟件系統中,用來衡量測試套件的有效性。
James McCaffrey 博士 供職於 Volt Information Sciences, Inc.,在該公司他負責管理對華盛頓州雷蒙德市沃什灣 Microsoft 總部園區的軟件工程師進行的技術培訓。他參與過多項 Microsoft 產品的研發工作,包括 Internet Explorer 和 MSN Search。McCaffrey 博士是《.NET Test Automation Recipes》(Apress, 2006) 的作者,您可通過以下電子郵箱地址與他聯繫:[email protected]。
衷心感謝以下 Microsoft 技術專家對本文的審閱:Paul Koch、Dan Liebling 和 Shane Williams
轉自:http://msdn.microsoft.com/zh-cn/magazine/hh148145.aspx