[转载]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方法了。