【IT168 专稿】Microsoft Visual Studio Team System集成了多种功能,这些功能用于创建高质量代码。其中一项功能用于实现单元编码测试。并在Microsoft ASP.NET内部运行测试代码。在本文中,首先从测试驱动开发技术开始讲解,因为这是敏捷开发人员所提倡的重点。
上一篇:使用VS Team Edition进行单元测试
·预期的错误
在某些测试中,开发人员可能需要提供一些错误条件。所以,可以标注测试方法为ExpectedException属性,并设定为预期的异常类型。在如下示例中,即使抛出DivideByZeroException异常,测试也能通过。
public class UnitTests
{
[TestMethod]
[ExpectedException(typeof(DivideByZeroException))]
public void DivideTest()
{
int x = 1; int y = 0;
MyUtils.Calc.Divide(x, y);
}
}
·数据驱动测试
为了彻底测试,并保证代码的完全覆盖率,尤其是测试所有边缘条件,有时候需要使用不同的输入参数和对同一段代码进行反复测试,以及针对不同的返回值的测试。除了编写每一种不同情况的测试代码之外,测试框架还能从数据库获取不同的测试参数。这种方法称为数据驱动测试。采用如下SQL脚本对建立名为TestData的Microsoft SQL Server Express Edition数据库,之后再将其清除。TestData数据库包含用于数据驱动测试的数据。
USE master
GO
DROP DATABASE TestData
GO
CREATE DATABASE TestData
GO
use TestData
GO
CREATE TABLE CalcTestData
(
x int NOT NULL,
y int NOT NULL,
expected int NOT NULL,
shouldpass bit NOT NULL
)
GO
INSERT INTO CalcTestData (x, y, expected, shouldpass)
values (1, 1, 2, 1)
INSERT INTO CalcTestData (x, y, expected, shouldpass)
values (1, 2, 1, 0)
GO
-- destroy.sql
USE master
GO
DROP DATABASE TestData
GO
同样使用如下命令脚本执行上述SQL脚本。这些命令脚本从测试运行配置的“Setup And Cleanup Scripts”选项卡引用,所以它们可以在测试运行之前/后执行。
sqlcmd -S .\SQLExpress -i create.sql
rem destroy.cmd
sqlcmd -S .\SQLExpress -i destroy.sql
最后,单元测试使用如下代码示例中的数据。注意SQL脚本必须部署到测试运行目录,然后才能由命令脚本执行。
public class UnitTests
{
private TestContext tc;
public TestContext TestContext
{
get { return tc; }
set { tc = value; }
}
[TestMethod]
[DataSource(
@"Provider=SQLOLEDB;Data Source=.\sqlexpress;
Integrated Security=SSPI;Initial Catalog=TestData",
"CalcTestData")]
[DeploymentItem("create.sql")]
[DeploymentItem("destroy.sql")]
public void DataDrivenAddTest()
{
int x = (int)TestContext.DataRow[0];
int y = (int)TestContext.DataRow[1];
int expected = (int)TestContext.DataRow[2];
int actual = MyUtils.Calc.Add(x, y);
bool shouldpass = (bool)TestContext.DataRow[3];
if (shouldpass)
Assert.AreEqual<int>(expected, actual);
else
Assert.AreNotEqual<int>(expected, actual);
}
}
DataSource属性设定关于连接字符串的信息,字符串指定包含数据驱动测试数据的测试数据库和表。这些数据对于TestContext对象的DataRow属性可用。在测试集合执行时,TestContext包含所有测试框架维护的有用信息。因为测试类提供TestContext类型的公共属性TestContext,所以测试框架提供当前TestContext对象的引用。每次调用测试都会读取CalcTestData表中的一行,TestContext类的DataRow属性表示每次调用的当前行。图11显示了测试的输出结果。

图11 数据驱动测试输出
·数据驱动测试的数据管理
这里有两个值得一提的相关问题。首先,为了降低最后一个示例的大量移动部分,建议不要使用配置文件抽象数据源信息。实际上,如果有多个开发人员运行测试,他们可能都使用不同的数据源信息。虽然本文并不会说明如何实现,但是DataSource属性允许可以完成。在线MSDN文档有关于这方面的解释。第二,每次运行测试时,在示例中可以使用SQL脚本轻松地创建和安装包含测试数据的数据库。当然,这项技术比较适合用于已经存在的测试数据库,而对于其它环境也许并不能良好工作。管理数据库是一个问题,并且没有适合所有情况的方法。但是抽象数据源信息到配置文件,提供了适合不同方法的灵活性。
·测试Web服务代码
开发人员不仅可以本地化运行单元测试代码,还能远程运行代码,例如Web服务。采用与本地代码相同的方法编写纯粹的Web服务测试驱动开发显然不太可能,因为测试Web服务时,实际上是本地测试Web服务代理。因为这并不是Web服务客户端的典型开发方法,所以开发人员不能真正编写调用代理的测试,然后再编写满足测试的代理。有经验的Web服务开发人员建议,首先生成Web服务描述语言(Web Services Description Language,WSDL)规则,然后再分别开发基于规则的Web服务和Web服务客户端实现。因为过于复杂,所以客户端代理代码和服务器端说明代码通常都工具自动生成。
然而,可以首先生成WSDL规则,再生成基于此规则的客户端代理,然后在测试中调用代理。由于Web服务还不存在,执行测试会失败。所以必须实现并部署按照WSDL规则的Web服务。直到最终传递给测试,生成Web服务都可能出现问题。虽然并不是纯粹的测试驱动开发,但是非常接近。在本示例中,使用Visual Studio传递最简单的路径,然后向解决方案添加Web服务项目,并对其进行测试。右击任意Web服务方法,通过与本地方法相同的途径生成测试。图12显示如何操作。这对于生成Web服务代理和使用该代理的测试有一定影响。如下代码中显示了生成的带有少量装饰性修改的测试方法。

图12 测试Web服务
public class WebUnitTests
{
...
[TestMethod()]
public void AddTest()
{
CalcWebService target = new CalcWebService();
int x = 1;
int y = 2;
int expected = 3;
int actual;
actual = target.Add(x, y);
Assert.AreEqual(expected, actual);
}
}
当运行测试时,与其它测试的运行并没有差别。在常规案例中,需要保证Web服务的宿主服务器在测试运行之前已经启动。但是本示例中,Visual Studio承载测试框架,并用于开发和承载Web服务(同本例),Visual Studio Web服务器(Cassini)按需求启动。另外,在常规案例中,如果重新部署Web服务,那么必须在新位置指定Web服务客户端配置信息文件。但是,这种情况下Visual Studio承载测试框架,并用于开发和承载Web服务,它自动实现这些操作。
当Web服务由Cassini承载,并且尝试在Visual Studio意外运行时(例如Mstest.exe、命令行测试工具、或者Team Foundation Server的Team Build),该测试就会出现问题。无论是Mstest.exe,还是Team Foundation Server的Team Build都不能启动Cassini,即使能够启动,它们也没有向代理传送Cassini所选动态端口的方法。在这个示例中,需要获取测试框架才能实现所有重要功能。为了通知测试框架启动Cassini监听专用虚拟目录(映射专用物理目录)的请求,需要应用AspNetDevelopmentServer属性。另外,需要使用WebServiceHelper类的TryUrlRedirection方法通知端口代理Cassini选择的监听端口,测试框架信息存储在TestContext中的。注意PathToWebRoot变量用于避免Cassini使用的硬编码物理地址。这个变量在Visual Studio“工具”菜单的“选项”设置,然后为设置应用程序根目录,如图13所示。

图13 设置PathToWebRoot变量
public class WebUnitTests
{
[TestMethod()]
[AspNetDevelopmentServer("mysettings",
@"%PathToWebRoot%\MyWebSite", "/MyWebSite")]
public void AddTest()
{
CalcWebService target = new CalcWebService();
int x = 1;
int y = 2;
int expected = 3;
int actual;
WebServiceHelper.TryUrlRedirection(
target, TestContext, "mysettings");
actual = target.Add(x, y);
Assert.AreEqual(expected, actual);
}
}
如果使用Mstest.exe在Visual Studio意外运行测试,那么它将启动Cassini,并正常访问Web服务,如图14所示。

图14 MSTest在ASP.NET内部运行测试
测试Web服务时,实际上是在本地进程的上下文中测试本地Web服务代理,再与远程Web服务通信。开发人员并没有测试Web服务的方法,或者它调用运行在宿主Web服务上下文中的方法。如果希望对运行在ASP.NET上下文中的测试进行编码,应该怎样做呢?可以在ASP.NET内部承载测试框架,并运行测试。这是当然可能的。为什么需要这样做?部分测试需要进行一些ASP.NET上下文方面的检查,例如会话。
考虑一个简单的示例,Web服务使用辅助方法添加两个数字,然后在ASP.NET会话中存储结果。
{
public static void AddPlacingResultInSession(int x, int y)
{
int ret = x + y;
HttpContext.Current.Session["lastsum"] = ret;
}
}
[WebService(Namespace = "http://example.org/calcservice")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class CalcWebService : System.Web.Services.WebService
{
[WebMethod]
public int Add2(int x, int y)
{
Helper.AddPlacingResultInSession(x, y);
return (int)Session["lastsum"];
}
}
如果右击Helper类的AddPlacingResultInSession方法,可以创建与前面类似的单元测试。这一次,需要在会话中指定正确的结果。
[HostType("ASP.NET")]
[AspNetDevelopmentServerHost("%PathToWebRoot%\\MyWebSite",
"/MyWebSite")]
[UrlToTest("http://localhost/MyWebSite")]
public void AddPlacingResultInSessionTest()
{
int x = 1; int y = 2;
int expected = 3;
TestProject.HelperAccessor.AddPlacingResultInSession(x, y);
Assert.AreEqual<int>(expected,
(int)HttpContext.Current.Session["lastsum"]);
}
只有执行了ASP.NET中的测试和被测试代码,测试才能成功(并且确实成功了!)。那么它如何工作的呢?当HostType("ASP.NET")和UrlToTest属性应用到前述测试方法时,起到了特殊作用。测试框架使用UrlToTest属性设定的URL指定目标Web应用程序。注意URL消息包含动态分配的端口号,本例中AspNetDevelopmentServerHost属性通知测试框架启动Cassini。之后,测试框架为目标Web应用程序更改web.config文件,并将其设置为针对VSEnterpriseHelper.axd端点路径的临时配置的HTTP模块和HTTP处理程序。安装的HTTP模块和处理程序使得测试框架称为目标Web应用程序的引导程序。通常,这种测试在ASP.NET内部运行。
·小结
如果测试代码贯穿开发生命周期,那么能更快抓住和修改bug,并且完成高质量编码。Visual Studio Team Edition的两个版本(for Software Developers和for Software Testers)包含对单元测试代码的支持。读者可以使用它们的功能实践测试驱动开发或者测试现有代码,代码甚至还能运行在远程Web应用程序中。