技术开发 频道

ASP.NET MVC架构下的测试驱动开发

IT168 专稿

一、引言


   本文旨在向你解释创建如何使用Visual Studio 2008进行单元测试。更具体地说,我不想泛泛地谈论单元测试的有关概念,而是想专注于讨论当构建ASP.NET MVC Web应用程序工程时如何在测试驱动开发环境下构建一个特定类型的单元测试。

   其实,并非所有的单元测试都是优秀的TDD测试。要想在测试驱动开发中应用单元测试,你必须能够执行以非常快的速度执行单元测试。然而,并非所有的单元测试都能满足这个要求。

   例如,Visual Studio针对ASP.NET网站提供了一种特定类型的单元测试支持。你必须在IIS或开发web服务器上下文中执行这个类型的单元测试。但是,当你进行测试驱动开发时,这并不是一个适当类型的单元测试,因为这个类型的单元测试速度太慢了。

   在本文中,我想向你展示构建用于测试驱动开发的单元测试的详细过程。我将详细地向你描述使用Visual Studio 2008单元测试框架的有关细节。此外,我还要讨论若干高级题目,例如测试私有方法和如何从命令行执行测试,等等。

   【注意】本文中所描述的大多数特征为Visual Studio 2008 Professional Edition所支持。但遗憾的是,这些特征却并不为Visual Web Developer所支持。因此,如果读者想了解关于Visual Studio 2008中每种版本对于单元测试特征的支持详情,请参考网址http://msdn2.microsoft.com/en-us/library/bb385902.aspx。

二、快速创建一个ASP.NET MVC Web应用程序示例

   首先,让我们创建一个新的ASP.NET MVC Web应用程序工程并且创建一个相应的测试工程。这一步是非常容易的。当你创建一个新的ASP.NET MVC Web应用程序工程时,系统会随后提示你是否创建一个新的Visual Studio测试工程,如图1所示。只要你保持图1顶部的单选按钮(即缺省的选项),那么你会看到一个新的测试工程自动地添加到你的方案上。





         图1—创建一个新的ASP.NET MVC Web应用程序工程和相应的单元测试工程 

   现在的问题是:既然你有一个测试工程,那么你该如何使用这个测试工程呢?

   当你创建一个新的ASP.NET MVC应用程序时,工程包括一个名字为HomeController的控制器。这个控制器有两个名字分别为Index()和About()的缺省方法。相应于该HomeController工程提供了一个文件名字为HomeControlleterTest的测试工程。这个测试文件包含两个测试方法,分别为Index()和About()。

   默认情况下,Index()和About()这两个测试方法内容为空(如图2所示)。接下来,你可以在这些方法中添加你的测试逻辑。



              图2—系统自动生成的测试工程中的About()测试方法为空

   假设我们要构建一个在线存储系统。比如说,你想创建一个Details页面用于显示一个特定产品的细节信息。然后,你要把一个包含ProductId的查询字符串传递到这个Details页面,并且要实现从数据库中检索产品细节信息,而且要把此信息显示到页面上。

   在良好的测试驱动开发实践中,在真正编码之前,你首先需要编写一个测试。你不是先编写任何应用程序代码,而是先编写相应于该代码的测试。为了创建一个成功的Details页面,必须满足下列测试要求:

   (1)如果没有把一个ProductId传递到该页面,则应该抛出一个异常
   (2)该ProductId应该用于从数据库中检索一个产品
   (3)如果不能从数据库中检索出一个相匹配的产品,那么应该抛出一个异常
   (4)Details视图应该能够顺利生成
   (5)Product数据应该被赋值给Details视图的ViewData结构
 

 

   接下来,我们将首先实现测试代码的编写。根据前面的第一条测试要求:如果没有把一个ProductId传递到该页面,则应该抛出一个异常。我们需要把一个新的单元测试添加到我们的测试工程。右击你的测试工程的Controllers文件夹,选择“添加→新的测试”。然后,选择单元测试模板(见图2),并且命名此该新的单元测试为ProductControllerTest。



               图2—添加一个新的单元测试

   在此,请诸位注意我们是如何创建一个新的单元测试的,因为存在多种方式可以错误地添加一个单元测试。例如,如果你右击Controllers文件夹并且选择“添加→新的测试”,那么你会看到一个单元测试向导。这个向导将生成一个单元测试,此测试将运行于一个web服务器上下文中。但是,这并不是我们想实现的。如果你看到如图3所示的对话框,那么,你要提醒自己你正在以一种错误的方式试图添加一个MVC单元测试。



           图3—无论何时看到这样一个对话框,请点击Cancel按钮!

   默认情况下,该ProductControllerTest将包含如列表1所示的唯一的一个测试方法。

   列表1—系统自动生成的最初的文件ProductControllerTest.cs 
 

[TestMethod]
public void TestMethod1()
{
//
// TODO: 在此添加测试逻辑
//…………
}


   现在,我们想修改这个测试方法以便它能够测试是否抛出一个异常—当Details页面要求的ProductId参数不能满足时。于是,我们创建如列表2所示的正确测试。

   列表2—修改后的文件ProductControllerTest.cs
 

[TestMethod]
[ExpectedException(typeof(ArgumentNullException), "Exception no ProductId")]
public void Details_NoProductId_ThrowException()
{
ProductController controller = new ProductController();
controller.Details(null);
}


 

 

   现在,让我来解释一下列表2中的有关测试编码。该方法使用两个属性加以修饰。其中,第一个属性[TestMethod]标识此方法为一个测试方法,第二个属性[ExpectedException]则建立针对于该测试的一个期望。如果执行该测试方法的过程中不抛出一个ArgumentNullException型异常,那么该测试失败。我们之所以想使该测试抛出一个异常是因为我们想实现当Details页面所要求的ProductId参数不能满足时抛出一个异常。

   接下来,测试方法正文部分包含了两个语句。其中,第一个语句创建了ProductController类的一个实例,第二个语句调用控制器的Details()方法。

   值得注意的是,目前在我们的MVC应用程序中,我们还没有创建这个ProductController类。因此,这个测试不会成功执行。但是,这正是我们所期望实现的。很显然,当使用测试驱动思想进行开发时,这正体现了这种开发思想的重要特征。首先,你要编写一个将会失败的测试,然后,你再编写代码来进一步修改这个测试,再运行测试,再修改,……,直到通过测试。

   因此,让我们运行上面的这个测试—而且,我们将会得到期望的失败结果。注意到,在代码编辑器窗口的顶部应该有一个包含有两个按钮的工具栏。这两个按钮用于运行测试。其中,第一个按钮支持在当前上下文中运行当前测试,而第二个按钮能够在当前方案中运行所有的测试(见图4)。



   图4—使用Visual Studio 2008测试工具栏

   现在,我们来详细分析一下点击这两个按钮有什么不同效果。在当前上下文运行测试将执行不同的测试依赖于你的鼠标光标在代码编辑器窗口中所在的位置。如果你的鼠标光标位于一个特定的测试方法中,那么将仅仅执行此方法。如果你的鼠标光标位于整个测试类中,那么该测试类中所有的测试都将被执行。如果当前焦点位于测试结果窗口中,那么将执行所有的测试(有关细节,请参考http://blogs.msdn.com/nnaderi/archive/2007/05/11/new-unit-testing-features-in-orcas-part-1.aspx)。

   实际上,我建议你应该总是努力避免使用鼠标点击按钮的方式。一方面,点击按钮速度太慢;另一方面,测试驱动开发要求所有执行测试过程必须相当迅速。因此,我推荐你使用下列这些组合键来执行测试:

   Ctrl-R,A—运行方案中的所有测试
   Ctrl-R,T—运行当前上下文中的所有测试
   Ctrl-R,N—运行当前命名空间中的所有测试
   Ctrl-R,C—运行当前类中的所有测试
   Ctrl-R,Ctrl-A—调试方案中的所有测试
   Ctrl-R,Ctrl-T—调试当前上下文中的所有测试
   Ctrl-R,Ctrl-N—调试当前命名空间中的所有测试
   Ctrl-R,Ctrl-C—调试当前类中的所有测试

   如果你使用Ctrl-R,A组合键运行我们刚刚创建的测试方法,那么它将失败。该测试甚至不会编译成功,因为我们还没有创建ProductController类或一个Details()方法。这正是我们接下来要做的。

   切换回到ASP.NET MVC工程,使用鼠标右击Controllers文件夹,然后选择“Add→New Item”。选择Web类型并且选择MVC控制器类。把新的控制器命名为ProductController并且点击“Add”按钮(或仅仅按一下回车键)。于是,创建一个包括一个Index()方法的新的控制器。

   现在,我们想编写尽可能少的代码仅使我们的单元测试运行通过就行。列表3中的ProductController类将能够通过我们的单元测试。

   列表3—ProductController.cs
 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace MvcApplication3.Controllers
{
public class ProductController : Controller
{
public void Details(int? ProductId)
{
throw new ArgumentNullException("ProductId");
}
}
}


   在此,列表3中的类ProductController包含一个方法名字为Details()。请注意,在此我略去了当你创建一个新的控制器时默认生成的Index()方法。于是,上面的Details()方法总是抛出一个ArgumentNullException异常。 

   在输入列表3中的代码后,按下键盘上的组合键Ctrl-R,A(你不需要切换回测试工程运行测试)。图5展示了我们的测试成功时的测试结果窗口。



                  图5—成功通过测试的绿色对号提示

   你可能会认为,你不是疯了吧?没错,当前情况下,我们的Details()方法总是抛出一个异常。在此,再次提醒你注意测试驱动开发的基本思想:目前情况下,你仅需专注于满足你的测试要求就行。以后的测试将迫使你进一步构建一个更为符合实际要求的控制器方法。
 

 

三、Visual Studio测试属性

   在上一节构建我们的测试时,我们需要使用下列两个属性:

   [TestMethod]—用于把一个方法标记为一个测试方法。当你运行你的测试时,仅标记有这个属性的方法才能够运行。
   [TestClass]—用于把一个类标记为一个测试类。当你运行你的测试时,仅标记有这个属性的类才能够运行。 

   当构建测试时,你总是使用[TestMethod]和[TestClass]属性。然而,还存在其它若干有用的(但是可选的)测试属性。例如,你可以使用下列属性对来建立和简化你的测试:

   [AssemblyInitialize]和[AssemblyCleanup]—分别用于标记那些在一个程序集中的所有测试执行之前或之后要执行的方法
   [ClassInitialize]和[ClassCleanup]—分别用于标记那些在一个类中的所有测试执行之前或之后要执行的方法
   [TestInitialize]和[TestCleanup]—分别用于标记那些在一个特定的测试方法之前或之后要执行的方法

   例如,你可能想创建一个虚构的HttpContext并使之应用于你所有的测试方法中。此时,你可以在一个标记有[ClassInitialize]属性的方法中建立该虚构的HttpContext,然后在一个标记有[ClassCleanup]属性的方法中释放此虚构的HttpContext。

   此外,还存在若干属性你可以用于提供关于测试方法的额外信息。当你操作成百上千的单元测试时,你需要通过排序和过滤等方法来管理这些测试。此时,下面这些属性就变得相当有用:

   [Owner]—指定一个测试方法的作者
   [Description]—提供一个测试方法的描述
   [Priority]—能够使你为一个测试指定一个整数优先权
   [TestProperty]—指定一个随意的测试属性

   你可以在测试视图窗口或测试列表编辑器中使用这些属性来排序和过滤测试。

   最后,还存在一个属性可以支持你当运行一个测试时忽略一个特定的测试方法。当你的一个测试出现问题并且你目前还不想处理该问题时,这个属性就变得相当有用的:

? [Ignore]—支持你临时性地禁用一个特定的测试。你可以把这个属性应用于一个测试方法或一个测试类之上。

四、 创建测试断言

   大多数情况下,当时你编写你的测试方法代码时你都会使用Assert类提供的方法。例如,大多数测试方法中的最后一行代码往往都使用Assert类来断言一个测试必须满足的条件,从而最终使该测试顺利通过。

Assert类支持下列静态方法:

   AreEqual—断言两个值是相等的
   AreNotEqual—断言两个值不是相等的
   AreNotSame—断言两个对象是不同的对象
   AreSame—断言两个对象是相同的对象
   Fail—断言一个测试失败
   Inconclusive—断言一个测试的结果是不确定的。Visual Studio在它自动生成的方法中包括了这个断言,要求你自己去实现
   IsFalse—断言一个给定条件表达式返回值False
   IsInstanceOfType—断言一个给定对象是一个指定类型的实例
   IsNotInstanceOfType—断言一个给定对象不是一个指定类型的一个实例
   IsNotNull—断言一个对象不是一个Null值
   IsNull—断言一个对象为一个Null值
   IsTrue—断言一个给定条件表达式返回值True
   ReplaceNullChars—在一个以\0结尾的字符串中使用\\0代替其中的Null字符

   当上面任何一个Assert方法失败时,该Assert类将抛出一个AssertFailedException异常。

   例如,假定你在编写一个单元测试来测试一个方法:此方法实现两个数求和。列表4中的测试方法使用了一个Assert方法检查是否被测试方法返回2+2相应的正确的结果。

   列表4–CalculateTest.cs
 

[TestMethod]
public void AddNumbersTest()
{
int result = Calculate.Add(2, 2);
Assert.AreEqual(result, 2 + 2);
}


   值得注意的是,有一个特定的名为CollectionAssert的类,用于测试与集合相关的断言。该CollectionAssert类支持下列静态方法:

   AllItemsAreInstancesOfType—断言一个集合中的每一项都属于一个指定的类型
   AllItemsAreNotNull—断言一个集合中的每一项都非空
   AllItemsAreUnique—断言一个集合中的每一项都是唯一的
   AreEqual—断言两个集合中的每一个对应项的值都相等
   AreEquivalent—断言两个集合中的每一个对应项的值都相等(但是第一个集合中的项的顺序可能与第二个集合中的项的顺序不相匹配)
   AreNotEqual—断言两个集合不是相等的
   AreNotEquivalent—断言两个集合不是相等的
   Contains—断言一个集合包含一个指定的项
   DoesNotContain—断言一个集合不包含一个指定的项
   IsNotSubsetOf—断言一个集合不是另一个集合的一个子集
   IsSubsetOf—断言一个集合是另一个集合的一个子集

   此外,还存在一个特别的名为StringAssert的类,专门用于实现有关于字符串的断言。该StringAssert类支持下列静态方法:

   Contains—断言一个字符串包含一个指定的子串
   DoesNotMatch—断言一个字符串不匹配一个指定的正规表达式
   EndsWith—断言一个字符串以一个指定的子串结束
   Matches—断言一个字符串匹配一个指定的正规表达式
   StartsWith—断言一个字符串以一个指定的子串开头

   最后,你可以使用[ExpectedException]属性来断言一个测试方法应该抛出一个特定类型的异常。在前面的例子中,我们就使用了该ExpectedException属性来测试是否一个NullProductId会致使一个控制器抛出一个ArgumentNullException类型的异常。
 

五、 从现有代码生成测试

   Visual Studio 2008支持你从现有代码自动地生成单元测试。为此,你可以右击一个类中的任何方法并且选择“Create Unit Tests…”选项。



               图6—从现有代码自动生成的一个单元测试

   一般说来,每一位测试驱动开发者都会不同程度地使用以前遗留(或别人提供)的现成的代码。所以,如果你需要在现有代码上添加单元测试的话,那么你可以利用这个选项来快速地创建必要的测试方法相应的基本代码部分。

   【注意】关于使用这个方法添加单元测试目前尚存在一个BUG。如果你在一个ASP.NET MVC Web应用程序工程的一个类上使用这个选项,那么,你会看到将打开一个单元测试向导。但遗憾的是,这个向导生成的单元测试是执行于一个web服务器的上下文环境下。显然,这个类型的单元测试是不适合于测试驱动开发的,因为它的执行需要花费太长的时间。因此,我推荐你仅当使用类库工程时才使用本节中所描述的方法生成单元测试。

六、 测试私有方法、属性和域

   当遵循良好的测试驱动开发思想进行开发时,你应当测试你的所有的代码,包括你的应用程序中定义的私有方法。那么,该如何测试你的测试工程中定义的私有方法呢?乍看起来,问题似乎是:不能从一个单元测试内部调用私有方法。

   针对上面这个问题存在两种解决方案.首先,Visual Studio 2008可以生成一个类以暴露被测试类的所有私有类型的成员。在Visual Studio 2008中,你可以从代码编辑器中右击任何类,然后选择菜单选项创建私有访问器(即“Create Private Accessor”)。选择这个菜单选项将生成一个新类。借助于这个新类,它能够把所有的私有方法、属性和域暴露为公共类型的方法、属性和域.

   例如,假定你想测试一个名字为Calculate的类中包含的一个名字为Subtract()的私有类型方法,那么,你可以右击这个类来生成一个访问器(Accessor),见图7。



               图7—创建一个私有类型的访问器(Accessor)

   在你创建该访问器后,你可以把它应用于你的单元测试代码中来测试该Subtract方法。例如,列表5中提供的单元测试将测试是否该subtract方法返回7–5的正确结果。

   列表5—CalculateTest.cs(访问器)
 

[TestMethod]
public void SubtractTest()
{
int result = Calculate_Accessor.Subtract(7, 5);
Assert.AreEqual(result, 7 - 5);
}

 
   注意:在列表5中,Subtract()方法在Calculate_Accessor类上而不是为Calculate类所调用.因为该Subtract()方法是私有类型的,所以你不能够在Calculate类上调用它。然而,生成的Calculate_Accessor类却恰到好处地暴露了该方法。

   如果你愿意,你可以使用命令行方式生成上面这个访问器(Accessor)类。Visual Studio提供了一个现成的命令行工具,名字为Publicize.exe,能够帮助你针对一个类的private类型成员生成一个对应的公共类型的成员。

   测试私有类方法的第二种方法是使用.NET反射原理。借助于反射原理,你可以绕过访问限制来调用一个类的任何类型的方法和任何类的属性。列表6提供的测试代理正是使用反射技术来调用私有的Calculate.Subtract()方法。

   列表6—CalculateTest.cs(反射原理)
 

[TestMethod]
public void SubtractTest()
{
MethodInfo method = typeof(Calculate).GetMethod("Subtract",
BindingFlags.NonPublic | BindingFlags.Static);
int result = (int)method.Invoke(null, new object[] { 7, 5 });
Assert.AreEqual(result, 7 - 5);
}


   列表6中的代码通过调用一个MethodInfo对象(用于描述Subtract方法)的Invoke()方法最终调用了私有的静态类型的Subtract()方法。(我建议你把这样的代码打包进一个工具类中以便日后把它轻松地重用于其它测试)。

七、 与测试窗口有关的问题

   我承认,我撰写本文的一个主要目的是我本人也为各种类型的测试窗口所疑惑不解。因此,我想干脆把它们整理一下。归纳来看,Visual Studio 2008共提供了三个与单元测试相关的窗口。

   第一个是测试结果窗口(见图8)。当你运行完你的测试时将显示这个窗口。你还可以通过选择菜单选项“测试—Windows—测试结果”来显示这个窗口。该测试窗口将显示运行过的每一个测试并且显示该测试是失败还是顺利通过测试



                                 图8—测试结果窗口

   如果你点击标记有“Test run completed”(即“测试运行成功”)或标记有“Test run failed”(即“测试运行失败”)的链接,那么,呈现在你面前的将是一个关于该测试运行情况的更详细信息的页面。

   第二个窗口是测试视图(TestView)窗口(见图9)。你可以使用菜单“测试—窗口—测试视图”来打开该测试视图窗口。该测试视图窗口能够列举出你的所有测试。你可以选择单个的测试并运行该测试。你还可以使用测试视图中特定的测试属性来过滤测试(例如,仅仅显示Stephen编写的测试)。



      图9—测试视图窗口

   第三个与测试相关的窗口是测试列表编辑器窗口(见图10)。你可以通过使用菜单“测试—窗口—测试列表编辑器”来打开这个窗口。这个窗口能够帮助你把你的所有测试组织成不同的列表。你可以创建新的测试列表,并且把相同中的测试添加到多个列表中。当你需要管理上百个测试时,创建多个测试列表将是非常有用的。



         图10—测试列表编辑器窗口

八、 管理测试运行

   当你执行你的单元测试超过25次以上时,你得到如图11所示的对话框.直到我观察到这个警告时,我才认识到:原来在你每次运行一个测试(每次你运行你的单元测试)时,Visual Studio都会为方案中所有的程序集创建一个单独的副本。

 

      图11—与测试运行有关的一条神秘消息

   如果你使用Windows资源管理器观察一下磁盘上你的应用程序方案文件夹,那么,你会注意到Visual Studio 2008为你自动地创建的一个名字为TestResults的文件夹。这个文件夹中针对每一个测试运行各包含相应的一个XML文件和一个子文件夹。

   请注意,你可以通过禁用测试发布来避免Visual Studio 2008针对每一个测试运行创建你的程序集的副本。为此,你可以修改你的测试运行配置文件。这仅需要选择菜单“测试—编辑测试—运行配置”即可。然后,选择“Deployment”选项卡并且在其中取消选择“Enable deployment”复选框。



                              图12—禁用测试发布功能

   有时,当你打开某个测试,然后打开“编辑测试运行配置”(Test—Edit Test Run Configurations)菜单项时,你会注意到出现到一条消息“不存在可用的测试运行配置”(No Test Run Configurations Available)。这种情况下,你需要在解决方案资源管理器窗口中右击你的方案,然后选择“添加—新项”来添加一个新的测试运行配置。当你添加一个新的测试运行配置文件之后,你即可以打开图12中所示的对话框。

   【注意】如果你禁用测试发布功能,那么,你可能无法再利用系统提供的代码覆盖(coverage)特征。当然,如果你不使用这个特征的话,则不用担心这件事情。

九、 从命令行运行测试

   有些情况下,你可能想从命令行上运行你的单元测试。例如,你可能不愿意使用像Visual Studio这样的集成开发环境而仅想使用记事本来书写你所有的代码;或者更可能的是,你想作为一个定制代码登记策略的一部分来自动地运行你的测试。

   只要打开Visual Studio 2008命令提示符(程序→Microsoft Visual Studio 2008→Visual Studio Tools→Visual Studio 2008 Command Prompt)即可以实现从命令行运行你的测试。在你打开该命令提示符后,导航到你的测试工程生成的程序集。例如:
 

Documents\Visual Studio 2008\ Projects \MyMvcApp\MyMvcAppTests\Bin\ Debug

 
   然后,执行下列命令运行你的测试:
 

mstest /testcontainer:MyMvcAppTests.dll


   发出上面这个命令将启动运行你的所有测试(见图13)。
 



            图13—从命令行运行你的单元测试

十、 总结

   最后再强调一下,本文的目的是为了帮助你更好地理解在进行测试驱动开发时如何使用VisualStudio 2008编写单元测试。Visual Studio的设计支持许多不同的类型测试并且并且针对许多不同的测试用户。单单就它所提供的测试选项(以及测试相关的窗口)来说,为数就相当惊人。总之,我希望你也同意我的观点:Visual Studio 2008的确是一个实现测试驱动开发的相当有效的开发环境。

   最后,如果你忽略本本中所有其他内容的话,我也建议你至少记住使用键盘组合键Ctrl-R,A来运行你的方案中的所有测试。
 

0
相关文章