一步一步学习ASP.NET MVC 1.0创建NerdDinner 范例程序,Part 27
本文根据《Professional ASP.NET MVC 1.0》中微软牛人Scott Guthrie 提供免费下载的第一章,一步一步演示如何通过ASP.NET MVC 1.0 正式版创建NerdDinner 范例程序。对了解如何使用最新的ASP.NET MVC 1.0框架创建Web Application 非常有帮助。本文由
http://forum.entlib.com 开源论坛小组提供。
本文继续学习之旅,一步一步通过ASP.NET MVC 1.0 实现NerdDinner 范例程序。
单元测试
让我们开发一套自动的单元测试来验证NerdDinner范例程序的功能,这样在将来,我们可以自信地修改和改进应用程序。
为什么需要单元测试?
某天早上,你突然灵光一现,对正在工作的应用程序有个新的想法。你认为如果你实现一个更新,将是整个应用程序得到极大的改进。这也许就是重构,精简代码、添加新的功能或者修复bug。
当你坐在电脑前时,你面临的一个问题是 – 做这些更改,到底有多安全?是否这些更新有副作用或者破坏一些功能?这些更新可能很简单,只需要几分钟就可以完成,但是是否需要数个小时来手动测试整个应用程序?是否有可能你忘记一些,并导致有问题的应用程序上线到生产环境?做这些更新是否真的值得所有的付出?
自动化单元测试提供安全性,让你不断地增强你的应用程序,并避免害怕更新代码。自动化测试快速验证应用程序的功能,让你变得更加自信,有能力改进代码。也有助于创建易于维护和长久生命周期的项目,从而产生更高的投资回报。
ASP.NET MVC框架使得单元测试更加容易和自然,也支持TDD(Test Driven Development)工作流,开发基于测试优先。
NerdDinner.Tests 项目
NerdDinner.Tests项目引用了NerdDinner应用程序项目的程序集,让我们轻松实现自动化测试,并验证应用程序的功能。
为Dinner模型类创建单元测试
添加一些测试到NerdDinner.Tests项目,验证我们在创建模型层(Model Layer)创建的Dinner类。
在测试项目创建一个新的文件夹 – Models,在这里我们存放一些模型相关的测试。接着,右键单击文件夹,选择Add->New Test菜单项,将弹出Add New Test对话框。
我们选择创建Unit Test单元测试,命名为DinnerTest.cs:
默认Visual Studio 的单元测试模板有一些代码,且有些杂乱。让我们清理代码如下:
using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace NerdDinner.Tests.Models
{
///<summary>
/// Summary description for DinnerTest
///</summary>
[TestClass]
public class DinnerTest
{
}
}
DinnerTest类中的[TestClass] 属性标识该类将包含测试,以及可选的初始化和代码。我们可以添加其他公有的方法,并设置 [TestClass] 属性。
下面是我们测试Dinnerlei的其中第一个(总共有2个)测试,第一个测试验证:如果一个新的Dinner对象创建过程中没有设置正确的属性,测试方法则认为Dinner对象是无效的。
接下来是第二个测试,该测试验证如果一个Dinner对象正确设置了所有属性,则Dinner对象是有效的。
[TestClass]
public class DinnerTest
{
[TestMethod]
public void Dinner_Should_Not_Be_Valid_When_Some_Properties_Incorrect()
{
//Arrange
Dinner dinner = new Dinner()
{
Title = "Test title",
Country = "USA",
ContactPhone = "BOGUS"
};
// Act
bool isValid = dinner.IsValid;
//Assert
Assert.IsFalse(isValid);
}
[TestMethod]
public void Dinner_Should_Be_Valid_When_All_Properties_Correct()
{
//Arrange
Dinner dinner = new Dinner
{
Title = "Test title",
Description = "Some description",
EventDate = DateTime.Now,
HostedBy = "ScottGu",
Address = "One Microsoft Way",
Country = "USA",
ContactPhone = "425-703-8072",
Latitude = 93,
Longitude = -92,
};
// Act
bool isValid = dinner.IsValid;
//Assert
Assert.IsTrue(isValid);
}
}
你应该注意到测试方法的名称非常清楚(甚至有点冗长)。我们这样命名是因为我们需要创建成千上百的测试方法,通过方法名称,我们可以迅速地了解的每一个方法的意图和行为(特别是在查看错误列表时)。测试方法的名称总是命名为正在测试的功能,我们使用Noun_Should_Verb 命名模式。
我们使用AAA测试模式创建测试方法 – 分别代表Arrange、 Act和Assert:
Arrange – 设置测试单元;
Act – 执行测试单元,并捕获结果;
Assert – 验证执行行为;
当我们编写测试时,应尽量避免有太多的单个的测试。每一个测试应该验证一个单一的概念(这样,可以轻松定位到错误的原因)。一个好的设计向导是尽量针对每一个测试有一个assert(断言)语句。如果你在一个测试方法中有多个assert语句,确保它们都在测试同一个概念。如不确定,则创建另外一个测试方法。
运行测试
Visual Studio 2008 专业版(或其他高级版本)包含了一个内置的测试运行器,可以在IDE中运行Visual Studio Unit Test项目。选择 Test -> Run -> All Tests in Solution 菜单项,运行所有的单元测试。或者将光标定位到一个特定的测试类或测试方法中,选择 Test -> Run -> Test in Current Context菜单项,运行部分单元测试。
下面我们将光标定位到DinnerTest类中,选择Test -> Run -> Test in Current Context 菜单项,运行我们刚才定义的2个测试方法。随后Test Results窗口自动在Visual Studio 中出现,我们将可以看到测试结果:
备注:VS 测试结果窗口默认没有显示类名称(Class Name)。可以右键点击Test Results 窗口,选择Add/Remove Columns菜单项,然后添加Class Name选项。
上述仅仅测试了一小部分,且都通过了测试。下面进行创建其他的测试,来验证特定的规则,并覆盖2个辅助方法 – IsUserHost() 和 IsUserRegistered() – 这是之前添加到Dinner类中的方法。针对Dinner类的这些测试让我们今后可以更容易和更安全地添加新的业务规则和验证。我们可以添加新的业务逻辑到Dinner类中,然后在几秒钟内验证这些更新是否破坏了任何之前的逻辑和功能。
认识到一个清楚的测试方法的名称有助于尽快理解政治测试的内容。另外,还建议使用 Tools->Options 菜单项,打开 Test Tools -> Test Execution 配置窗口,选择“Double-clicking a failed or inconclusive unit test result displays the point of failure in the test”复选框。当在测试结果窗口双击错误记录时,会立即跳到断言错误的地方。
创建DinnersController 单元测试
下面创建一个单元测试验证DinnersController 的功能。右键点击测试项目中的Controllers文件夹,选择Add->New Test 菜单项,创建一个单元测试,命名为DinnersControllerTest.cs。
创建2个测试方法验证DinnersController中的Details() action方法。第一个将验证当请求一个存在的Dinner对象时,一个视图将返回。第二个将验证如果请求一个不存在的Dinner对象时,NotFound视图将返回。
[TestClass]
public class DinnersControllerTest
{
[TestMethod]
public void DetailsAction_Should_Return_View_For_ExistingDinner()
{
// Arrange
var controller = new DinnersController();
// Act
var result = controller.Details(2) as ViewResult;
// Assert
Assert.IsNotNull(result, "Expected View");
}
[TestMethod]
public void DetailsAction_Should_Return_NotFoundView_For_BogusDinner()
{
// Arrange
var controller = new DinnersController();
// Act
var result = controller.Details(999) as ViewResult;
// Assert
Assert.AreEqual("NotFound", result.ViewName);
}
}
运行测试,上述2个测试都会失败:
查看错误信息,失败的原因是因为DinnersRepository类不能连接到数据库。NerdDinner 范例程序使用连接字符串连接到SQL Server数据库。因为NerdDinner.Tests项目还没有正确配置数据库连接信息。
只需要打开NerdDinner.Tests项目中app.config 配置文件,配置数据库连接信息就可以了:
<connectionStrings>
<addname="NerdDinnerConnectionString"connectionString="Data Source=localhost;Initial Catalog=NerdDinner;Integrated Security=True"
providerName="System.Data.SqlClient" />
</connectionStrings>
单元测试代码使用真实的数据库,可能带来一些挑战,如:
1. 它会显著降低单元测试的执行速度。执行测试的时间越长,你会越少执行单元测试。理想情况是,你希望单元测试能够在几秒内执行完成,像编译项目一样自然。
2. 它使设置和清理过程变得复杂。你希望每一个单元测试独立、互不依赖。如果连真实的数据库进行测试,你需要关心数据状态,并在不同的测试直接进行数据复位。
下一节将演示“依赖注入(Dependency Injection)”设计模式,可以帮助我们解决这些问题,并避免在测试过程中使用真实的数据库。