一步一步学习ASP.NET MVC 1.0创建NerdDinner 范例程序,Part 29
本文根据《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 范例程序。恭喜恭喜,本文终于是本系列文章的最后一篇了。在此,感谢 美女程序员、Jacky 的友情协助。
创建Edit Action方法的单元测试
下面创建DinnersController的编辑功能的单元测试。首先,测试Edit Action方法的HTTP-GET版本:
//
// GET: /Dinners/Edit/2
public ActionResult Edit(int id)
{
Dinner dinner = dinnerRepository.GetDinner(id);
if (!dinner.IsHostedBy(User.Identity.Name))
return View("InvalidOwner");
// 使用ViewData
// ViewData["Countries"] = new SelectList(PhoneValidator.Countries, dinner.Country);
// return View(dinner);
// 使用ViewModel
return View(new DinnerFormViewModel(dinner));
}
备注:上述代码中DinnerFormViewModel是之前我们定义的一个Model类。
我们将创建一个测试:当请求一个有效的Dinner对象时,验证返回的DinnerFormViewModel对象。
[TestMethod]
public void EditAction_Should_Return_View_For_ValidDinner()
{
// Arrage
var controller = CreateDinnersController();
// Act
var result = controller.Edit(2) as ViewResult;
// Assert
Assert.IsInstanceOfType(result.ViewData.Model, typeof(DinnerFormViewModel));
}
如果现在运行测试,发现测试会失败,这是因为Edit方法在访问User.Identity.Name属性,执行Dinner.IsHostedBy()检查时,抛出null引用异常。
Controller基类的User对象封装了登录用户的详细信息,在运行时创建Controller时,ASP.NET MVC填充该对象。因为我们测试DinnersController时,没有运行在web-server的环境,因此User 对象没有设置,导致null引用异常。
模仿User.Identity.Name属性
Mocking Framework可以帮忙我们动态创建虚假的依赖对象,支持测试工作。例如,在Edit Action 方法的测试中,我们可以使用一个Mocking Framework,动态创建User对象,DinnersController 将使用该对象来模拟一个用户名。这样在运行测试时,可以避免null引用的发生。
下载后,在NerdDinner.Tests项目中添加对Moq.dll 程序集的引用。
接着在测试类中添加一个重载的CreateDinnersControllerAs(username) 辅助方法,接收username参数,该参数模仿DinnersController实例中的User.Identity.Name 属性。
DinnersController CreateDinnersControllerAs(string userName)
{
var mock = new Mock<ControllerContext>();
mock.SetupGet(p => p.HttpContext.User.Identity.Name).Returns(userName);
mock.SetupGet(p => p.HttpContext.Request.IsAuthenticated).Returns(true);
var controller = CreateDinnersController();
controller.ControllerContext = mock.Object;
return controller;
}
上述代码使用Moq创建一个Mock对象,虚拟一个ControllerContext对象(该对象是ASP.NET MVC传递给Controller类,公布运行时对象,如User、Request、Response和Session)。调用Mock的SetupGet方法,表示ControllerContext的HttpContext.User.Identity.Name 属性应该返回username字符串,该字符串是传递给辅助方法的参数。
我们可以模拟ControllerContext的任何属性和方法。为了证明这一点,我们也向Request.IsAuthenticated 属性添加了SetupGet() 的调用(该属性对于下面的测试是不需要的,但是可以证明如何模拟Request属性)。最后,将模拟的ControllerContext实例赋值给辅助方法需要返回DinnersController对象。
下面使用上述辅助方法编写单元测试,用不同的用户测试Edit方法:
[TestMethod]
public void EditAction_Should_Return_View_For_ValidDinner()
{
// Arrage
var controller = CreateDinnersControllerAs("EntLib.com");
// Act
var result = controller.Edit(2) as ViewResult;
// Assert
Assert.IsInstanceOfType(result.ViewData.Model, typeof(DinnerFormViewModel));
}
[TestMethod]
public void EditAction_Should_Return_InvalidOwnerView_When_InvalidOwner()
{
// Arrange
var controller = CreateDinnersControllerAs("NotOwnerUser");
// Act
var result = controller.Edit(2) as ViewResult;
// Assert
Assert.AreEqual(result.ViewName, "InvalidOwner");
}
现在通过所有测试:
测试UpdateModel()
我们已经创建了测试HTTP-GET版本的Edit Action方法,下面继续创建测试HTTP-POST版本的Edit Action方法:
//
// POST: /Dinners/Edit/2
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection formValues)
{
Dinner dinner = dinnerRepository.GetDinner(id);
if (!dinner.IsHostedBy(User.Identity.Name))
return View("InvalidOwner");
try
{
UpdateModel(dinner);
dinnerRepository.Save();
return RedirectToAction("Details", new { id = dinner.DinnerID });
}
catch
{
ModelState.AddRuleViolations(dinner.GetRuleViolations());
// 使用ViewData
// ViewData["countries"] = new SelectList(PhoneValidator.Countries, dinner.Country);
// return View(dinner);
// 使用ViewModel
return View(new DinnerFormViewModel(dinner));
}
}
上述Action方法使用了Controller基类的UpdateModel() 辅助方法,使用该辅助方法绑定表单提交的值到Dinner对象实例。
下面的2个测试演示了如何提供表单提交的值给UpdateModel() 辅助方法使用。通过创建和填充一个FormCollection对象,接着赋值给Controller的ValueProvider属性,来实现测试。测试方法代码如下:
[TestMethod]
public void EditAction_Should_Redirect_When_Update_Successful()
{
// Arrange
var controller = CreateDinnersControllerAs("EntLib.com");
var formValues = new FormCollection() {
{ "Title", "Another value" },
{ "Description", "Another description" }
};
controller.ValueProvider = formValues.ToValueProvider();
// Act
var result = controller.Edit(1, formValues) as RedirectToRouteResult;
// Assert
Assert.AreEqual("Details", result.RouteValues["Action"]);
}
[TestMethod]
public void EditAction_Should_Redisplay_With_Errors_When_Update_Fails()
{
// Arrange
var controller = CreateDinnersControllerAs("EntLib.com");
var formValues = new FormCollection() {
{ "EventDate", "Bogus date value!!!"}
};
controller.ValueProvider = formValues.ToValueProvider();
// Act
var result = controller.Edit(1, formValues) as ViewResult;
// Assert
Assert.IsNotNull(result, "Expected redisplay of view");
Assert.IsTrue(result.ViewData.ModelState.Count > 0, "Expected errors");
}
其中第一个测试验证:当成功保存后,浏览器重定向到Details Action方法。第二个测试验证:当提交无效的表单参数值时,重新显示带错误消息的Edit视图。现在,再次运行测试,结果如下:
单元测试总结
我们已经完成了对Controller类进行单元测试的核心概念。我们可以使用这些技术轻松创建好几百简单测试,验证应用程序的功能。
因为Controller和Model测试不需要真实的数据库,这样可以非常快和容易运行。我们可以在几秒钟执行几百个自动化测试,并立即获得信息 – 是否更新破坏了现有的逻辑。这样,让我们有信心持续改进、重构和优化应用程序。
在本章的最后部分,我们介绍了测试相关技术,但并不表示测试是开发流程的最后阶段。相反,你应该在开发流程中尽早编写自动化测试。这样,你可以在开发过程中及时得到反馈结果,帮助你仔细思考应用程序的业务场景,并指导你设计清晰分层的、松耦合的应用程序。
《Professional ASP.NET MVC 1.0》这本书的随后章节将介绍Test Driven Development(TDD),已经如何在ASP.NET MVC中使用。TDD是一个迭代的开发过程。通过TDD,首先编写验证将要实现的业务功能的测试。编写单元测试,可以帮助你清晰理解功能和如何工作。仅仅在完成测试代码的编写之后,才可是实现对应的实际功能。因为你已经思考了这些功能如何工作的业务场景,你可以更好地理解需求,以及如何最好地实现。当你完成了业务代码的编写之后,你可以重新运行测试,立即获得关于功能是否工作正常的反馈。
NerdDinner范例程序总结
NerdDinner范例应用程序终于完成了,已经可以发布了。
我们使用了大量的ASP.NET MVC功能来创建NerdDinner范例程序。希望这一开发过程演示了ASP.NET MVC核心功能是如何工作的,已经如何在一个应用程序中集成这些功能。