asp.net

位置:IT落伍者 >> asp.net >> 浏览文章

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


发布日期:2023年11月03日
 
ASP.NET MVC架构下的测试驱动开发

引言

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

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

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

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

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

快速创建一个ASPNET MVC Web应用程序示例

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

—创建一个新的ASPNET MVC Web应用程序工程和相应的单元测试工程

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

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

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

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

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

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

)如果没有把一个ProductId传递到该页面则应该抛出一个异常

)该ProductId应该用于从数据库中检索一个产品

)如果不能从数据库中检索出一个相匹配的产品那么应该抛出一个异常

)Details视图应该能够顺利生成

)Product数据应该被赋值给Details视图的ViewData结构

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

—添加一个新的单元测试

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

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

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

列表—系统自动生成的最初的文件ProductControllerTestcs

[TestMethod]

public void TestMethod()

{

//

// TODO: 在此添加测试逻辑

//…………

}

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

列表—修改后的文件ProductControllerTestcs

[TestMethod]

[ExpectedException(typeof(ArgumentNullException) Exception no ProductId)]

public void Details_NoProductId_ThrowException()

{

ProductController controller = new ProductController();

controllerDetails(null);

}

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

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

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

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

—使用Visual Studio 测试工具栏

现在我们来详细分析一下点击这两个按钮有什么不同效果在当前上下文运行测试将执行不同的测试依赖于你的鼠标光标在代码编辑器窗口中所在的位置如果你的鼠标光标位于一个特定的测试方法中那么将仅仅执行此方法如果你的鼠标光标位于整个测试类中那么该测试类中所有的测试都将被执行如果当前焦点位于测试结果窗口中那么将执行所有的测试(有关细节请参考unittestingfeaturesinorcaspartaspx)

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

CtrlRA—运行方案中的所有测试

CtrlRT—运行当前上下文中的所有测试

CtrlRN—运行当前命名空间中的所有测试

CtrlRC—运行当前类中的所有测试

CtrlRCtrlA—调试方案中的所有测试

CtrlRCtrlT—调试当前上下文中的所有测试

CtrlRCtrlN—调试当前命名空间中的所有测试

CtrlRCtrlC—调试当前类中的所有测试

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

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

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

列表—ProductControllercs

using System;

using SystemCollectionsGeneric;

using SystemLinq;

using SystemWeb;

using SystemWebMvc;

namespace MvcApplicationControllers

{

public class ProductController : Controller

{

public void Details(int? ProductId)

{

throw new ArgumentNullException(ProductId);

}

}

}

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

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

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

你可能会认为你不是疯了吧?没错当前情况下我们的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—在一个以\结尾的字符串中使用\\代替其中的Null字符

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

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

列表–CalculateTestcs

[TestMethod]

public void AddNumbersTest()

{

int result = CalculateAdd( );

AssertAreEqual(result + );

}

值得注意的是有一个特定的名为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 支持你从现有代码自动地生成单元测试为此你可以右击一个类中的任何方法并且选择Create Unit Tests…选项

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

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

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

测试私有方法属性和域

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

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

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

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

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

列表—CalculateTestcs(访问器)

[TestMethod]

public void SubtractTest()

{

int result = Calculate_AccessorSubtract( );

AssertAreEqual(result );

}

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

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

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

列表—CalculateTestcs(反射原理)

[TestMethod]

public void SubtractTest()

{

MethodInfo method = typeof(Calculate)GetMethod(Subtract

BindingFlagsNonPublic | BindingFlagsStatic);

int result = (int)methodInvoke(null new object[] { });

AssertAreEqual(result );

}

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

与测试窗口有关的问题

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

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

—测试结果窗口

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

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

—测试视图窗口

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

—测试列表编辑器窗口

管理测试运行

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

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

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

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

—禁用测试发布功能

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

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

从命令行运行测试

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

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

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

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

mstest /testcontainer:MyMvcAppTestsdll

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

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

总结

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

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

               

上一篇:ASP.NET页面刷新和定时跳转

下一篇:使用ASP.Net中的自定义控件