ASP.NET MVC单元测试时如何对含有ModelState.IsValid的Action进行测试

  下面的例子来至Asp.Net MVC 2的项目模板。

  首先是一个实体类:

实体类
[PropertiesMustMatch("Password", "ConfirmPassword", ErrorMessage = "The password and confirmation password do not match.")]
public class RegisterModel
{
[Required]
[DisplayName(
"User name")]
public string UserName { get; set; }

[Required]
[DataType(DataType.EmailAddress)]
[DisplayName(
"Email address")]
public string Email { get; set; }

[Required]
[ValidatePasswordLength]
[DataType(DataType.Password)]
[DisplayName(
"Password")]
public string Password { get; set; }

[Required]
[DataType(DataType.Password)]
[DisplayName(
"Confirm password")]
public string ConfirmPassword { get; set; }
}

 

  然后是Action:

代码
[HttpPost]
public ActionResult Register(RegisterModel model)
{
if (ModelState.IsValid)
{
// Attempt to register the user
MembershipCreateStatus createStatus = MembershipService.CreateUser(model.UserName, model.Password, model.Email);

if (createStatus == MembershipCreateStatus.Success)
{
FormsService.SignIn(model.UserName,
false /* createPersistentCookie */);
return RedirectToAction("Index", "Home");
}
else
{
ModelState.AddModelError(
"", AccountValidation.ErrorCodeToString(createStatus));
}
}

// If we got this far, something failed, redisplay form
ViewData["PasswordLength"] = MembershipService.MinPasswordLength;
return View(model);
}

 

  如果你对这个Action写单元测试,你会发现没办法测试输入不完整的情况,比如下面的代码:

单元测试
[TestMethod()]
public void RegisterTest()
{
AccountController target
= new AccountController()
RegisterModel model
= new RegisterModel();
ActionResult actual;
actual
= target.Register(model);
var view
= actual as ViewResult;
Assert.IsNotNull(view);
Assert.IsNotNull(view.ViewData.ModelState[
"PasswordLength"]);
}

 

  这个单元测试不会跑完,因为注册的时候由于用户名是null,会抛出异常。因为这个判断:

if (ModelState.IsValid)

 

没有起作用,原因是Asp.Net MVC框架会在调用这个Action之前进行模型验证,由于单元测试直接测试这个Action,并没有进行模型验证的步骤,所以ModelState.IsValid仍然是默认值true,这个场景下无法验证传入的参数是否符合预期。

      解决办法有2个,第一是编写单元测试的人知道模型是否正确,只要改变ModelState.ISValid的值即可。如下代码:

方法一
/// <summary>
///Register 的测试
///</summary>
[TestMethod()]
public void RegisterTest()
{
AccountController target
= new AccountController()
RegisterModel model
= new RegisterModel();
ActionResult actual;
target.ViewData.ModelState.AddModelError(
"", "模型没有正确赋值");
actual
= target.Register(model);
var view
= actual as ViewResult;
Assert.IsNotNull(view);
Assert.IsNotNull(view.ViewData.ModelState[
"PasswordLength"]);
}

 

通过增加一个错误信息,ModelState.IsValid值就会被置位false,这样就可以测试Action中模型不正确的流程。这种解决办法的思路是模型验证已经由微软Asp.Net MVC团队测试过了,无需再测试。如果你需要测试你配置的Attribute是否正确,可以在另外的地方测试,这和Action无关。

  另外一种思路就是主动验证模型,并把验证结果添加到ModelState集合中去,这也就是Asp.Net MVC框架内部所作的工作。如果使用.Net 4.0,在System.ComponentModel.DataAnnotations命名空间中新增了ValidationContext类,可以在任何需要的地方对模型进行验证。这样只需要简单的写个帮助类,在单元测试中手动调用这个方法即可验证模型:

模型验证帮助类
public static void AddValidationErrors(this ModelStateDictionary modelState, object model)
{
var context
= new ValidationContext(model, null, null);
var results
= new List<ValidationResult>();
Validator.TryValidateObject(model, context, results,
true);
foreach (var result in results)
{
var name
= result.MemberNames.First();
modelState.AddModelError(name, result.ErrorMessage);
}
}

 

如果使用.Net 3.5,很遗憾,没有这个类,但是可以使用Asp.Net MVC内部框架的验证机制。Controller类中定义了一个方法:

protected internal bool TryValidateModel(object model); 这个方法对正确的模型,返回true,错误的返回false。这个方法并不是公开的,可以公开一个新的方法调用它。

public bool InvokeValidateModel(object model)
{
return TryValidateModel(model);
}

 

最后代码如下:  

最终代码
[TestMethod()]
public void RegisterTest()
{
AccountController target
= new AccountController()
RegisterModel model
= new RegisterModel();
ActionResult actual;
target.InvokeValidateModel(model);
actual
= target.Register(model);
var view
= actual as ViewResult;
Assert.IsNotNull(view);
Assert.IsNotNull(view.ViewData.ModelState[
"PasswordLength"]);
}

 

为了方便,可以把InvokeValidateModel方法定义在一个继承Controller的父类中。

  通过上面两种办法,现在我们可以正确的测试包含了ModelState.IsValid代码的Action方法了。

posted @ 2010-11-26 12:03  wenhx  阅读(4497)  评论(4编辑  收藏  举报