共用方式為


本文章是由機器翻譯。

測試運行

使用 TestApi 進行錯誤注入測試

James McCaffrey

下載代碼示例

錯誤注入測試是指有意向待測試的應用程式中注入錯誤,然後運行該應用程式以檢驗其錯誤處理情況的過程。錯誤注入測試可採取多種不同的形式。在本月的專欄中,我將介紹如何使用 TestApi 庫的元件,在運行時向 .NET 應用程式中引入錯誤。

要想瞭解我在本專欄中所講述的內容,最好是看一下圖 1 所示的螢幕擷取畫面。該螢幕擷取畫面顯示我正在一個名為 TwoCardPokerGame.exe 的虛擬 .NET WinForm 應用程式上進行錯誤注入測試。一個名為 FaultHarness.exe 的 C# 程式正在命令 shell 中運行。它改變了待測試應用程式的正常行為,所以當使用者第三次按一下標記為 Evaluate 的按鈕時,應用程式將引發異常。在這種情況下,Two Card Poker 應用程式不能妥善地處理應用程式異常,從而導致系統生成的訊息方塊。

圖 1 運行中的錯誤注入測試

讓我們進一步看看此方案,考慮一些相關細節。從命令 shell 啟動 FaultHarness.exe 時,工具會在後臺準備分析代碼,該代碼截取 TwoCard­PokerGame.exe 的正常代碼執行。這一過程稱為錯誤注入會話。

錯誤注入會話使用 DLL 來啟動對調用應用程式 button2_Click 方法的監視,該方法是標記為 Evaluate 的按鈕的事件處理常式。錯誤注入會話已經過配置,這樣,當使用者前兩次按一下 Evaluate 按鈕時,應用程式按代碼編寫的方式運行,但第三次按一下時,錯誤會話會導致應用程式引發 System.ApplicationException 類型的異常。

錯誤會話記錄會話活動並對一組檔進行日誌記錄,以測試主機。請注意,在圖 1 中,前兩次按一下應用程式 Deal-Evaluate 可工作正常,但第三次按一下生成異常。

接下來,我將簡要介紹待測試的虛擬 Two Card Poker Game 應用程式,提供並詳細說明圖 1 所示的 FaultHarness.exe 程式碼,並就何時適合使用錯誤注入測試以及何時更適合使用其他技術提供一些提示。雖然 FaultHarness.exe 程式本身十分簡單,大多數複雜工作由 TestApi DLL 在後臺執行,但理解和修改我在此處提供的代碼來滿足您自己的測試方案需求要求您充分瞭解 .NET 程式設計環境。也就是說,即使您是 .NET 初學者,您也應當能夠輕鬆理解我介紹的內容。我相信,您將發現探討錯誤注入的趣味性,這對於您的工具集來說可能也是有益的補充。

待測試的應用程式

我使用的待測試虛擬應用程式是一個簡單而卻具有代表性的 C# WinForm 應用程式,它類比一種稱為 Two Card Poker 的假想紙牌遊戲。該應用程式由兩個主要元件組成:TwoCardPokerGame.exe 提供 UI,TwoCardPokerLib.dll 提供基礎功能。

為了創建遊戲 DLL,我啟動了 Visual Studio 2008,然後從“檔”|“新建專案”對話方塊中選擇 C# 類庫範本。我將該庫命名為 TwoCardPokerLib。图 2 提供了該庫的整體結構。TwoCardPokerLib 的代碼太長,無法在本文中完整地提供。本文隨附的代碼下載中提供了 TwoCardPokerLib 庫的完整原始程式碼以及 FaultHarness 錯誤注入工具。

圖 2 TwoCardPokerLib 庫

using System;
namespace TwoCardPokerLib {
  // -------------------------------------------------
  public class Card {
    private string rank;
    private string suit;
    public Card() {
      this.rank = "A"; // A, 2, 3, . . ,9, T, J, Q, K
      this.suit = "c"; // c, d, h, s
    }
    public Card(string c) { . . . }
    public Card(int c) { . . . }
    public override string ToString(){ . . . }
    public string Rank { . . . }
    public string Suit { . . . }
    public static bool Beats(Card c1, Card c2) { . . . }
    public static bool Ties(Card c1, Card c2) { . . . }
  } // class Card

  // -------------------------------------------------
  public class Deck {
    private Card[] cards;
    private int top;
    private Random random = null;

    public Deck() {
      this.cards = new Card[52];
      for (int i = 0; i < 52; ++i)
        this.cards[i] = new Card(i);
      this.top = 0;
      random = new Random(0);
    }

    public void Shuffle(){ . . . }
    public int Count(){ . . . } 
    public override string ToString(){ . . . }
    public Card[] Deal(int n) { . . . }
    
  } // Deck

  // -------------------------------------------------
  public class Hand {
    private Card card1; // high card
    private Card card2; // low card
    public Hand(){ . . . }
    public Hand(Card c1, Card c2) { . . . }
    public Hand(string s1, string s2) { . . . }
    public override string ToString(){ . . . }
    private bool IsPair() { . . . }
    private bool IsFlush() { . . . }
    private bool IsStraight() { . . . }
    private bool IsStraightFlush(){ . . . }
    private bool Beats(Hand h) { . . . }
    private bool Ties(Hand h) { . . . }
    public int Compare(Hand h) { . . . }
    public enum HandType { . . . }
    
 } // class Hand

} // ns TwoCardPokerLib

應用程式 UI 代碼

完成基礎 TwoCardPokerLib 庫代碼後,我創建了一個虛擬 UI 元件。 我使用 C# WinForm 應用程式範本在 Visual Studio 2008 中創建了一個新專案,並將我的應用程式命名為 TwoCardPokerGame。

使用 Visual Studio 設計器,我將一個 Label 控制項從工具箱集合中拖到應用程式設計圖面上,並將該控制項的 Text 屬性由“textBox1”修改為“Two Card Poker”。然後,我另外添加了兩個 Label 控制項(“Your Hand”和“Computer’s Hand”)、兩個 TextBox 控制項、兩個 Button 控制項(“Deal”和“Evaluate”)和一個 ListBox 控制項。 我沒有改變八個控制項中任何一個控制項的預設控制項名稱,如 textBox1、textBox2 和 button1 等。

準備好設計後,按兩下 button1 控制項使 Visual Studio 為該按鈕生成一個事件處理常式框架,並在代碼編輯器中載入檔 Form1.cs。 此時,我在解決方案資源管理器視窗中按右鍵 TwoCardPokerGame 專案,然後從上下文功能表中選擇“添加引用”選項,並指向檔 TwoCardPokerLib.dll。 在 Form1.cs 中,我添加了一個 using 語句,以便不必完全限定庫中的類名稱。

接下來,我向應用程式添加了四個類作用域靜態物件:

namespace TwoCardPokerGame {
  public partial class Form1 : Form {
    static Deck deck;
    static Hand h1;
    static Hand h2;
    static int dealNumber; 
...

物件 h1 是針對使用者的 Hand,h2 是針對電腦的 Hand。 然後,我向 Form 構造函數添加了一些初始化代碼:

public Form1() {
  InitializeComponent();
  deck = new Deck();
  deck.Shuffle();
  dealNumber = 0;
}

Deck 構造函數創建一副撲克牌,按照從梅花 A 到黑桃 K 的順序共計 52 張,Shuffle 方法
隨機排列這副撲克牌中紙牌的順序。

接下來,我向 button1_Click 方法添加代碼邏輯,如圖 3 所示。 對於兩手牌中的每一手,我調用 Deck.Deal 方法從 deck 物件中刪除兩張牌。 然後,我將這兩張牌傳遞給 Hand 構建函數,並在 TextBox 控制項中顯示這手牌的分值。 請注意,button1_Click 方法通過在 ListBox 控制項中顯示消息來處理任何異常。

图 3 处理纸牌

private void button1_Click(
  object sender, EventArgs e) { 

  try  {
    ++dealNumber;
    listBox1.Items.Add("Deal # " + dealNumber);
    Card[] firstPairOfCards = deck.Deal(2);
    h1 = new Hand(firstPairOfCards[0], firstPairOfCards[1]);
    textBox1.Text = h1.ToString();

    Card[] secondPairOfCards = deck.Deal(2);
    h2 = new Hand(secondPairOfCards[0], secondPairOfCards[1]);
    textBox2.Text = h2.ToString();
    listBox1.Items.Add(textBox1.Text + " : " + textBox2.Text);
  }
  catch (Exception ex) {
    listBox1.Items.Add(ex.Message);
  }
}

接下來,在 Visual Studio 設計器視窗中按兩下 button2 控制項,以自動生成該控制項的事件處理常式
框架。 我添加了一些簡單代碼來比較兩個 Hand 物件,並在 ListBox 控制項中顯示一條消息。 請注意 button2_Click 方法並不直接處理任何異常:

private void button2_Click(
  object sender, EventArgs e) {
  int compResult = h1.Compare(h2);
  if (compResult == -1)
    listBox1.Items.Add(" You lose");
  else if (compResult == +1)
    listBox1.Items.Add(" You win");
  else if (compResult == 0)
    listBox1.Items.Add(" You tie");

  listBox1.Items.Add("-------------------------");
}

錯誤注入工具

在創建如圖 1 所示的錯誤注入工具前,我將關鍵 DLL 下載到測試主機上。 這些 DLL 是名為 TestApi 的 .NET 庫集合的一部分,可在 testapi.codeplex.com 中找到。

TestApi 庫是與軟體測試相關的實用工具的集合。 TestApi 庫包含一組託管代碼錯誤注入 API。 (有關這些 API 的更多資訊,請訪問 blogs.msdn.com/b/ivo_manolov/archive/2009/11/25/9928447.aspx。)我下載最新的錯誤注入 API 版本,在本例中為 0.4 版,然後解壓縮下載內容。 我將簡要地介紹下載內容和放置錯誤注入庫的位置。

0.4 版支援對使用 .NET Framework 3.5 創建的應用程式進行錯誤注入測試。 TestApi 庫正處於開發過程中,所以應查看 CodePlex 網站來瞭解我在本文中提供的技術是否有更新。 此外,您需要在 Bill Liu 的博客中查看是否有更新和提示,Bill Liu 是 TestApi 錯誤注入庫的主要開發人員,網址是 blogs.msdn.com/b/billliu/

為了創建錯誤注入工具,我在 Visual Studio 2008 中創建了一個新專案,然後選擇 C# 主控台應用程式範本。 我將該應用程式命名為 FaultHarness,並向該程式範本添加了一些最簡短的代碼(參見圖 4)。

圖 4 FaultHarness

using System;
namespace FaultHarness {
  class Program {
    static void Main(string[] args) {
      try {
        Console.WriteLine("\nBegin TestApi Fault Injection environmnent session\n");

        // create fault session, launch application

        Console.WriteLine("\nEnd TestApi Fault Injection environment session");
      }
      catch (Exception ex) {
        Console.WriteLine("Fatal: " + ex.Message);
      }
    }
  } // class Program
} // ns

我按 <F5>鍵構建並運行工具框架,該操作在 FaultHarness 根資料夾中創建了一個 \bin\Debug 資料夾。

TestApi 下載包含兩個關鍵元件。 第一個是 TestApiCore.dll,位於解壓縮下載的 Binaries 資料夾中。 我將該 DLL 複製到 FaultHarness 應用程式的根目錄中。 然後在解決方案資源管理器視窗中按右鍵 FaultHarness 專案,選擇“添加引用”,並指向 TestApiCore.dll。 接著,我將一個 Microsoft.Test.FaultInjection 的 using 語句添加到我的錯誤工具代碼頂部,以便該工具代碼能直接訪問 TestApiCore.dll 中的功能。 此外,我添加了一個 System.Diagnostics 的 using 語句,因為我希望從該命名空間訪問 Process 和 ProcessStartInfo 類,我將在稍後為您介紹這一點。

錯誤注入下載中的第二個關鍵元件是名為 FaultInjectionEngine 的資料夾。 該資料夾包含 32 位和 64 位版本的 FaultInjectionEngine.dll。 我將整個 Fault­InjectionEngine 資料夾複製到包含 FaultHarness 可執行檔的資料夾中,在本例中為 C:\FaultInjection\FaultHarness\bin\Debug\。 我使用的 0.4 版錯誤注入系統要求 FaultInjectionEngine 資料夾的位置與可執行工具的位置相同。 此外,系統要求待測試的二進位應用程式位於與可執行工具相同的資料夾中,因此我將 TwoCardPokerGame.exe 和 TwoCard­PokerLib.dll 檔複製到 C:\FaultInjection\FaultHarness\bin\Debug\ 中。

總而言之,使用 TestApi 錯誤注入系統時,最好生成工具框架並加以運行,從而創建 \bin\Debug 工具目錄,然後將 TestApiCore.dll 檔放入工具根目錄,將 FaultInjectionEngine 資料夾放入 \bin\Debug,同樣將待測試的二進位應用程式(.exe 和 .dll)放入 \bin\Debug。

使用 TestApi 錯誤注入系統要求您指定待測試的應用程式、待測試應用程式中將觸發錯誤的方法、觸發錯誤的條件以及將觸發的錯誤類型:

string appUnderTest = "TwoCardPokerGame.exe";
string method = 
  "TwoCardPokerGame.Form1.button2_Click(object, System.EventArgs)";
ICondition condition =
  BuiltInConditions.TriggerEveryOnNthCall(3);
IFault fault =
  BuiltInFaults.ThrowExceptionFault(
    new ApplicationException(
    "Application exception thrown by Fault Harness!"));
FaultRule rule = new FaultRule(method, condition, fault);

請注意,因為系統要求待測試應用程式位於與可執行工具相同的資料夾中,所以待測試的可執行應用程式的名稱不需要指向其位置的路徑。

指定將觸發注入錯誤的方法名稱是 TestApi 錯誤注入初學者常見的問題根源。該方法名稱必須完全符合 Name­space.Class.Method(args) 格式。我的首選方法是利用 ildasm.exe 工具檢查待測試的應用程式,説明自己確定觸發方法的簽名。從特定 Visual Studio 工具命令 shell 中啟動 ildasm.exe,指向待測試的應用程式,然後按兩下目標方法。图 5 顯示了一個利用 ildasm.exe 檢查 button2_Click 方法簽名的示例。

圖 5 利用 ILDASM 檢查方法簽名

指定觸發方法簽名時,不要使用方法返回類型,不要使用參數名稱。得到正確的方法簽名有時需要反復嘗試。例如,第一次嘗試確定目標 button2_Click 時,我使用:

TwoCardPokerGame.Form1.button2_Click(object,EventArgs)

我必須將其更正為:

TwoCardPokerGame.Form1.button2_Click(object,System.EventArgs)

TestApi 下載包括一個 Documentation 資料夾,該資料夾包含提供正確指導的概念文檔,指導使用者如何正確構造不同類型的方法簽名,包括構造函數、泛型方法、屬性和重載運算子。 在這裡,我確定的目標是位於待測試應用程式中的方法,但原本也可以確定基礎 Two­CardPokerLib.dll 中的方法為目標,例如:

string method = "TwoCardPokerLib.Deck.Deal(int)"

指定觸發方法後,下一步是指定將錯誤注入待測試應用程式的條件。 在本例中,我使用的是 TriggerEveryOnNthCall(3),正如您所看到的,每當第三次調用觸發方法時它便注入一個錯誤。 TestApi 錯誤注入系統有一組簡潔的觸發條件,包括 TriggerIfCalledBy(method) 和 TriggerOnEveryCall 等。

指定觸發條件後,下一步是指定將注入待測試系統的錯誤的類型。 我使用的是 BuiltInFaults.ThrowExceptionFault。 除了異常錯誤外,TestApi 錯誤注入系統具有內置的返回類型錯誤,允許在運行時將錯誤返回值注入待測試的應用程式。 例如,這將導致觸發方法返回值 -1(可能不正確):

IFault f = BuiltInFaults.ReturnValueFault(-1)

在指定錯誤觸發方法、條件和錯誤類型後,下一步是創建一個新 FaultRule,並將該規則傳遞給新 FaultSession:

FaultRule rule = new FaultRule(method, condition, fault);
Console.WriteLine(
  "Application under test = " + appUnderTest);
Console.WriteLine(
  "Method to trigger injected runtime fault = " + method);
Console.WriteLine(
  "Condition which will trigger fault = On 3rd call");
Console.WriteLine(
  "Fault which will be triggered = ApplicationException");
FaultSession session = new FaultSession(rule);

所有的預備工作就緒後,編寫錯誤工具代碼的最後一部分是以程式設計方式在錯誤會話環境中啟動待測試的應用程式:

ProcessStartInfo psi = 
  session.GetProcessStartInfo(appUnderTest);
Console.WriteLine(
  "\nProgrammatically launching application under test");
Process p = Process.Start(psi);
p.WaitForExit();
p.Close();

當您執行錯誤工具時,該工具會在錯誤會話中啟動待測試的應用程式,同時 FaultInjection­Engine.dll 會監視當觸發條件為真時觸發方法的調用情況。在這裡,測試是手動執行的,但也可以在錯誤會話中運行測試自動化。

當錯誤會話運行時,有關該會話的資訊將記錄到目前的目錄中,該目錄是包含可執行錯誤工具和可執行的待測試應用程式的目錄。您可以檢查這些日誌檔,説明解決在開發錯誤注入工具時可能遇到的任何問題。

討論

我在此處提供的示例和說明應當可以帶您入門,説明您為自己的待測試應用程式創建錯誤注入工具。與作為軟體發展過程一部分的任何活動一樣,您具有有限資源,因此,您應分析執行錯誤注入測試的得與失。對某些應用程式而言,創建錯誤注入測試所需的工作可能並不值得,但也存在許多錯誤注入測試至關重要的測試方案。設想一下控制醫療設備或飛行系統的軟體。在此類情況下,應用程式必須絕對可靠,並且能正確處理各種異常錯誤。

錯誤注入測試無疑具有諷刺意味。也就是說,如果您能預料可能發生異常的情況,則常常可以在理論上以程式設計方式防範異常,並進行測試來獲得該防範行為的正確行為。不過,即使在這類情況下,錯誤注入測試對阻止異常的發生也是非常有用的。另外,可以注入難以預測的錯誤,例如 System.OutOfMemoryException。

錯誤注入測試與變化測試相關,有時二者會發生混淆。在變化測試中,有意將錯誤注入待測試的系統中,但隨後針對錯誤系統執行現有測試套件,以檢查測試套件是否捕獲了新產生的錯誤。變化測試是一種評估測試套件有效性的方法,並最終擴大了測試案例範圍。正如您在本文中看到的一樣,錯誤注入測試的主要目的是確定待測試系統是否能正確地處理錯誤。

Scripto 供職于 Volt Information Sciences, Inc.,在該公司他負責管理對華盛頓州雷蒙德市沃什灣 Microsoft 總部園區的軟體工程師進行的技術培訓。McCaffrey 博士是《.NET 軟體測試自動化之道》(Apress,2006)一書的作者,您可以通過以下位址與他聯繫:jammc@microsoft.com

衷心感謝以下技術專家參與本文的審閱:Bill LiuPaul Newson