Visual Studio 2015高级编程(第6版)
上QQ阅读APP看书,第一时间看更新

11.1 第一个测试用例

测试用例的编写很难实现自动化,这是因为测试用例必须对应被开发软件的功能。事实上,人们经常争论是否自动化除了最简单的单元测试之外的其他测试用例。但是,在测试过程的某些阶段中,可以通过工具生成一些代码存根。为了说明这一点,先来看一段相对简单的代码片段,学习如何编写测试用例代码。Subscription类有一个CurrentStatus公共属性,该属性把当前的订阅状态返回为一个枚举值。

VB

      Public Class Subscription
          Public Enum Status
              Temporary
              Financial
              Unfinancial
              Suspended
          End Enum
          Public Property PaidUpTo As Nullable(Of Date)
          Public ReadOnly Property CurrentStatus As Status
              Get
                  If Not Me.PaidUpTo.HasValue Then Return Status.Temporary
                  If Me.PaidUpTo > Now Then
                      Return Status.Financial
                  Else
                      If Me.PaidUpTo >= Now.AddMonths(-3)Then
                          Return Status.Unfinancial
                      Else
                          Return Status.Suspended
                      End If
                  End If
              End Get
          End Property
      End Class

C#

      public class Subscription
      {
          public enum Status
          {
              Temporary,
              Financial,
              Unfinancial,
              Suspended
          }
          public DateTime? PaidUpTo { get; set; }
          public Status CurrentStatus
          {
              get
              {
                  if (this.PaidUpTo.HasValue == false)
                      return Status.Temporary;
                  if (this.PaidUpTo > DateTime.Today)
                      return Status.Financial;
                  else
                  {
                      if (this.PaidUpTo >= DateTime.Today.AddMonths(-3))
                          return Status.Unfinancial;
                      else
                          return Status.Suspended;
                  }
              }
          }
      }

从上面的代码片段可以看出,需要从4个代码路径对CurrentStatus属性进行测试。为此,必须在一个新的测试项目中创建另一个SubscriptionTest测试类,在该类中添加一个测试方法,该方法包含的代码用于实例化一个Subscription对象,设置PaidUpTo属性,检查CurrentStatus属性是否包含正确的结果。接着,继续添加测试方法,直到执行并测试了涉及该属性的每一个代码路径。

在Visual Studio的以前版本中,可以通过向导生成给定任务的单元测试。出于许多原因(主要是因为智能生成单元测试是很复杂的,这么做会带来安全假象),在Visual Studio 2012中已经取消了该功能。但是,Visual Studio 2015包含一个新工具IntelliTest,它有助于生成单元测试代码。

这意味着,从实用的角度看,可以创建一个测试项目和许多方法,它们很容易通过一些基本步骤来运行类。参见本章后面的“IntelliTests”一节。然而,即使有一个工具可以帮助生成单元测试,也仍需要知道是什么使某个方法变成单元测试。Visual Studio提供了一个运行时引擎,该引擎可用于运行测试用例,监控其工作进度,报告测试结果。因此,开发人员只需要编写代码来测试存在疑问的属性即可。

要查看测试类的基本模板,需要确保在Solution Explorer中选择测试项目,然后选择Project | Add Unit Test。这会创建一个测试类和单个测试方法。Unit Test模板仅包括一个基本的单元测试类,该类仅包含单个方法,如下面的代码示例所示。对于该示例,已经将测试类命名为SubscriptionTest(而非默认的UnitTest1),以表明这是一个测试类:

VB

      Imports Microsoft.VisualStudio.TestTools.UnitTesting
      <TestClass()>
      Public Class SubscriptionTest
          <TestMethod()>
          Public Sub TestMethod1()
          End Sub
      End Class

C#

      using System;
      using Microsoft.VisualStudio.TestTools.UnitTesting;
      [TestClass]
      public class SubscriptionTest
      {
          [TestMethod]
          public void TestMethod1()
          {
          }
      }

虽然有很多技术可用来编写自己的单元测试,但是应该注意两个主要的理念。第一个理念是,如果项目中有大量的单元测试,那么很快就会变得难以管理这些单元测试。为了解决该问题,建议使用命名约定。如你所料,可以使用许多不同的命名约定,但流行的一种约定是MethodName_ StateUnderTest_ExpectedBehavior。这种简单的命名约定确保可以轻松地查找和标识测试用例。

第二个理念是使用Arrange/Act/Assert范例处理每个测试。首先设置并初始化用于测试中的值(Arrange部分),然后执行测试的方法(Act部分),最后确定测试的结果(Assert部分)。如果遵循该方法,最终就会得到类似于如下的单元测试:

VB

      <TestMethod()>
      Public Sub CurrentStatus_NothingPaidUpToDate_TemporaryStatus()
         ' Arrange
         Dim s as New Subscription()
         s.PaidUpTo = Nothing
         ' Act
         Dim actual as Subscription.Status = s.CurrentStatus
         ' Assert
         Assert.Inconclusive()
      End Sub

C#

      [TestMethod]
      public void CurrentStatus_NullPaidUpToDate_TemporaryStatus()
      {
         // Arrange
         Subscription s = new Subscription();
         s.PaidUpTo = null;
         // Act
         Subscription.Status actual = s.CurrentStatus;
         //Assert
         Assert.Inconclusive();
      }

在进一步学习之前,运行此测试用例以查看发生的情况,方法是在代码窗口右击该用例并选择Run Tests。此时会打开Test Explorer,如图11-1所示。

图11-1

上下文菜单仅是选择并运行测试用例的一种方式。还有一个Test菜单,其中包含Run子菜单,可用于执行所有或选择的测试。或者,可以直接打开Test Explorer窗口,并使用链接运行所有或选择的测试(参见图11-1)。除了这些方法之外,还可以从主工具栏中选择Debug Tests选项,在代码中设置断点并在调试器中运行测试用例。

从图11-1中可以看到,测试用例返回了一个不确定的结果。从本质上讲,这表明该测试是不完整的或者其结果不可信赖,因为某些修改会使该测试无效。结果显示了测试的基本信息、结果和其他有用的环境信息,如计算机名和测试的执行时间、开始和结束时间。

在创建此单元测试时,手动插入Assert.Inconclusive语句。要完成此单元测试,必须实际地执行适当的结果分析,以确保顺利通过测试。具体实现方式是使用Assert.AreEqual替换Assert.Inconclusive语句,如下面的代码所示:

VB

      <TestMethod()>
          Public Sub CurrentStatus_NothingPaidUpToDate_TemporaryStatus ()
              Dim target As Subscription = New Subscription
              Dim actual As Subscription.Status
              actual = target.CurrentStatus
              Assert.AreEqual(Subscription.Status.Temporary, actual, _
                              "Subscription.CurrentStatus was not set correctly.")
          End Sub

C#

      [TestMethod()]
      public void CurrentStatus_NullPaidUpToDate_TemporaryStatus ()
      {
          Subscription target = new Subscription();
          Subscription.Status actual;
          actual = target.CurrentStatus;
          Assert.AreEqual(Subscription.Status.Temporary, actual,
                          "Subscription.CurrentStatus was not set correctly.");
      }

尽管从到目前为止所完成的工作中还不能明显看出来,但需要知道完整的测试可划分为4个类别之一:Failed Tests、Passed Tests、Skipped Tests和Not Run Tests。可以运行所有的测试、只运行特定类别中的测试或者仅运行所选择的测试。Test Explorer顶部的Run链接包含一个下拉列表,从中可以选择要运行的测试类别。要选择运行个别的测试,可以单击所需的测试(使用标准的Ctrl+单击或Shift+Ctrl+单击操作,在第一个测试后添加其他测试),然后右击并选择Run Selected Tests。修正造成测试失败的代码之后,单击Run All按钮,以重新运行这些测试用例,并产生成功的结果,如图11-2所示。

图11-2

关于单元测试需要注意一件事。简单来说,单元测试方法的默认行为是“通过”。改变此行为的方式是向该方法添加Assert语句,而具体的理念是如果一条Assert语句失败,则单元测试就被认为已经“失败”。然而,手动创建全新的单元测试时,其中不存在任何断言,这意味着单元测试不会开始“失败”。为了解决此问题,在创建单元测试时会自动在其中放入Assert.Inconclusive语句。当移除该Assert.Inconclusive语句时,就表明测试用例已经完成。

在这个示例中,我们仅练习了一个代码路径,应该添加更多的测试用例,以充分地练习其他3个代码路径。尽管可以为已经创建的一个测试方法添加额外的断言,但这并不是编写单元测试的最佳实践。通常的方式是让每个测试方法仅测试一个方面。这意味着(理想情况下)该方法中只有一个Assert。

这样做的原因是,更加细粒度的测试意味着如果测试失败,则造成失败的原因通常更加显而易见。此外,需要注意该方法在第一个失败的Assert语句之后没有继续执行。如果方法中有多个断言,则更难确定造成失败的原因。虽然如此,方法中通常仍然有两个或三个断言,并且有一个可以传入Assert语句的参数,作为在测试失败时显示的消息。

11.1.1 使用特性标识测试

在进一步讨论单元测试之前,先考虑一下在Visual Studio 2015中如何进行测试。前面曾提到,所有的测试用例都必须存储在测试类中,而测试类必须位于一个测试项目中,那么到底是什么把方法、类或者项目区分为包含了测试用例的呢?先从测试项目开始,在底层的XML项目文件中,测试项目文件实际上与普通的类库项目文件之间没有任何区别。事实上,唯一的不同之处在于项目的类型。在生成测试项目时,它会输出一个标准的.NET类库程序集。这里的关键区别在于,Visual Studio把它看成一个测试项目,并自动分析出项目中的测试用例,对各种测试窗口进行填充。

测试过程中使用的类和方法都标记了对应的特性。测试引擎通过这些特性枚举一个程序集中全部的测试用例。

1.TestClass特性

每一个测试用例都必须位于测试类(使用TestClass特性进行标记)中。该特性仅用于把测试用例与要测试的类和成员对应,但后面将看到使用测试类对测试用例进行分组的好处。在对Subscription类的测试中创建了一个SubscriptionTest测试类,并标记了TestClass特性。由于Visual Studio使用特性定位包含测试用例的类,因此类的名称是不相关的。然而,采用良好的命名约定(如为要测试的类添加Test后缀)将便于管理大量的测试用例。

2.TestMethod特性

每一个测试用例都被标记了TestMethod特性,Visual Studio使用该特性枚举可执行的测试列表。在本例中,SubscriptionTest类中的CurrentStatus_NullPaidUpToDate_TemporaryStatus方法标记了TestMethod特性。同样,方法的实际名称是不相关的,因为Visual Studio只使用特性。尽管如此,在各个测试窗口列出测试用例时会用到方法的名称,因此测试方法也应该使用有意义的名称。在查看测试结果时,这一点尤其重要。

11.1.2 其他测试特性

如前所述,Visual Studio中的单元测试子系统是使用特性来区分测试用例的。此外,还可以使用其他各种特性为测试用例提供更多信息。这些信息可以通过测试用例的Properties窗口或者其他测试窗口来访问。本节介绍可以应用于测试方法的描述性特性。

1.Description特性

因为测试用例使用的是测试方法的名称,所以许多测试都拥有类似的名称,这些名称不足以区分测试的功能。要解决这个问题,可以使用Description特性,它接受一个String类型的参数,可用于在测试方法中提供和测试用例有关的其他信息。

2.Owner特性

Owner特性也接受一个String类型的参数。该特性用于指明是谁拥有、编写或者在使用某个测试用例。

3.Priority特性

Priority特性用于指定测试用例的相对优先级,它接受一个Integer类型的参数。尽管测试框架不使用该特性,但是如果为测试用例指定优先级顺序,则在测试用例运行失败或者未完成时可以更有效地处理测试用例。

4.TestCategory特性

TestCategory特性接受一个String类型的参数,给测试标识一个用户定义的类别。与Priority特性一样,TestCategory特性也被Visual Studio忽略,但可用于排序和分组相关的项。一个测试用例可以属于多个类别,但每个测试用例都必须有一个TestCategory特性。

5.WorkItem特性

WorkItem特性可以在工作项跟踪系统(如Team Foundation Server)中把测试用例链接到一个或多个工作项上。为测试用例指定一个或多个WorkItem特性意味着在对现有功能进行修改时,可以对测试用例进行复查。详见第57章介绍的Team Foundation Server。

6.Ignore特性

给测试方法应用Ignore特性,可以临时禁止运行它。带有Ignore特性的方法不会运行,也不会显示在测试运行的结果列表中。

可以把Ignore特性应用于测试类,关闭该类中的所有测试方法。

7.Timeout特性

测试用例可能会因为各种原因而失败,例如性能测试可能要求某个功能必须在一个特定的时间段内完成。除了通过编写复杂的多线程测试在达到该时限时终止测试用例以外,也可以对测试用例使用Timeout特性和超时值(以毫秒为单位),如下面的代码所示。这可以确保在抵达时限时测试用例会失败。

VB

      <TestMethod()>
      <Owner("Mike Minutillo")>
      <Description("Tests the functionality of the Current Status Property")>  <Priority(3)>
      <Timeout(10000)>
      <TestCategory("Financial")>
      Public Sub CurrentStatusTest()
          Dim target As Subscription = New Subscription
          Dim actual As Subscription.Status
          actual = target.CurrentStatus
          Assert.AreEqual(Subscription.Status.Temporary, actual, _
                          "Subscription.CurrentStatus was not set correctly.")
      End Sub

C#

      [TestMethod()]
      [Owner("Mike Minutillo")]
      [Description("Tests the functionality of the Current Status Method")]
      [Priority(3)]
      [Timeout(10000)]
      [TestCategory("Financial")]
      public void CurrentStatusTest()
      {
          Subscription target = new Subscription();
          Subscription.Status actual;
          actual = target.CurrentStatus;
          Assert.AreEqual(Subscription.Status.Temporary, actual,
                          "Subscription.CurrentStatus was not set correctly.");
      }

这段代码为原始的CurrentStatusTest方法使用了这些特性,演示了这些特性的用法。除了提供测试用例的功能和编写者相关的额外信息外,还把该测试用例的优先级设置为3,类别设置为Financial。最后,这段代码指定,如果测试用例的执行时间超过10s(10 000ms),就表明该测试用例失败。

11.1.3 单元测试和Code Lens

单元测试具备一些额外的优势,超过了第4章所述的Code Lens功能。图11-3演示了一个单元测试的代码,在第一次打开测试类时,这些代码会显示在代码编辑器中。

图11-3

References链接的左边是一个菱形的蓝色小图标。该图标的工具提示表示测试尚未运行。实际上,这意味着还没有为这个会话运行测试。Visual Studio的多次执行之间不会保存任何信息,表示该测试可能过去运行过。

执行测试后,图标会变化。它如何变化取决于测试的结果。图11-4是在跳过一个测试时显示的图标(如执行了Assert.Inconclusive时)。

图11-4

图标不仅是测试状态的可视化表示。当单击如图11-5所示的图标时,会看到测试结果的额外信息。这类似于显示在Test Explorer中的信息(参见图11-2)。

图11-5

在细目窗格的底部有两个额外的链接。可以使用Run链接在普通模式下运行测试,也可以使用Debug链接在调试模式下运行测试。

当测试成功时,将显示一个绿色的图标,如图11-6所示。测试的额外细节已经更新,但很容易再次运行或调试测试。

图11-6

Code Lens功能属于单元测试,超出了测试类本身。图11-7包含了一些代码,本章编写的测试在这些代码上运行。

图11-7

代码中有两个指示器,调用特定的属性或方法时,这些指示器表示单元测试的执行情况。PaidUpTo属性上面的第一个链接表明,一个单元测试调用了PaidUpTo属性,该测试成功了。CurrentStatus属性上面的指标表示,两个使用CurrentStatus属性的单元测试中,只有一个通过了。当点击这一指标时,就会显示测试列表,包括成功和不成功的测试,如图11-8所示。

图11-8