测试运行

使用 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。

错误注入测试与变化测试相关,有时二者会发生混淆。在变化测试中,有意将错误注入待测试的系统中,但随后针对错误系统执行现有测试套件,以检查测试套件是否捕获了新产生的错误。变化测试是一种评估测试套件有效性的方法,并最终扩大了测试案例范围。正如您在本文中看到的一样,错误注入测试的主要目的是确定待测试系统是否能正确地处理错误。

James McCaffrey 博士 供职于 Volt Information Sciences, Inc.,在该公司他负责管理对华盛顿州雷蒙德市沃什湾 Microsoft 总部园区的软件工程师进行的技术培训。他参与过多项 Microsoft 产品的研发工作,包括 Internet Explorer 和 MSN Search。McCaffrey 博士是《.NET 软件测试自动化之道》(Apress,2006)一书的作者,您可以通过以下地址与他联系:jammc@microsoft.com

衷心感谢以下技术专家对本文的审阅:Bill LiuPaul Newson