Tuesday, February 23, 2016

Unit Testing Asp.net MVC View Model Validation

ViewModel attribute validation is a key feature of ASP.NET MVC, but is often not a tested one.  For the most part developers verify controller actions, pass in a model, mock the ModelState.IsValid property and call it good, never checking to make sure if the model is validating correctly.  One of the biggest reasons is developers just don’t know how, it’s not difficult, but it’s not very intuitive so here is a quick tutorial.

First we take a view model with some basic validation attributes

   1:  public class RegisterViewModel
   2:  {
   3:      [Required]
   4:      [EmailAddress]
   5:      [Display(Name = "Email")]
   6:      public string Email { get; set; }
   7:   
   8:      [Required]
   9:      [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
  10:      [DataType(DataType.Password)]
  11:      [Display(Name = "Password")]
  12:      public string Password { get; set; }
  13:   
  14:      [DataType(DataType.Password)]
  15:      [Display(Name = "Confirm password")]
  16:      [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
  17:      public string ConfirmPassword { get; set; }
  18:  }

Next we create a simple reusable testing context class

   1:  public abstract class ViewModelValidation_Context<T> where T : new()
   2:  {
   3:      public T Target;
   4:      public List<ValidationResult> ActualMessages;
   5:      public bool Actual;
   6:   
   7:      [SetUp]
   8:      public void SetUp()
   9:      {
  10:          Context();
  11:          Because();
  12:      }
  13:   
  14:      public virtual void Context()
  15:      {
  16:          Target = new T();
  17:          ActualMessages = new List<ValidationResult>();
  18:      }
  19:   
  20:      public virtual void Because()
  21:      {
  22:          var context = new ValidationContext(Target, null, null);
  23:          Actual = Validator.TryValidateObject(Target, context, ActualMessages, true);
  24:      }
  25:  }

Notice this test context class is a little different than what I have used in the past, we are predefining the Because() method.  In this case regardless of the class we are testing we are still going to run what is really the meat of this class, we create a Validation Context and then passes it to System.ComponentModel.DataAnnotations.Validator.TryValidateObject and this returns if the view model passed validation and and validation messages.

Next lets put this into action with a test to validate our model

   1:  [TestFixture]
   2:  public class When_Testing_RegisterViewModel_Validation_Test : ViewModelValidation_Context<RegisterViewModel>
   3:  {
   4:      public override void Context()
   5:      {
   6:          base.Context();
   7:          Target.Email = "TestEmail@email.com";
   8:          Target.Password = "TestPass";
   9:          Target.ConfirmPassword = "TestPass";
  10:      }
  11:   
  12:      [Test]
  13:      public void Passed_Validation_Test()
  14:      {
  15:          Assert.IsTrue(Actual);
  16:      }
  17:   
  18:      [Test]
  19:      public void Has_No_ValidationMessages_Test()
  20:      {
  21:          Assert.IsFalse(ActualMessages.Any());
  22:      }
  23:  }

The model validation requires we have a valid email, we have a password, a confirm password, and password and confirm password match, in this case everything is good and we get a true for is valid and no validation massages.

Next lets test an invalid model

   1:  [TestFixture]
   2:  public class When_Testing_RegisterViewModel_Validation_With_NoPassword_Test : ViewModelValidation_Context<RegisterViewModel>
   3:  {
   4:      public override void Context()
   5:      {
   6:          base.Context();
   7:          Target.Email = "TestEmail@email.com";
   8:          Target.Password = string.Empty;
   9:          Target.ConfirmPassword = "TestPass";
  10:      }
  11:   
  12:      [Test]
  13:      public void Fail_Validation_Test()
  14:      {
  15:          Assert.IsFalse(Actual);
  16:      }
  17:   
  18:      [Test]
  19:      public void Has_No_ValidationMessages_Test()
  20:      {
  21:          Assert.IsTrue(ActualMessages.Any());
  22:      }
  23:   
  24:      [TestCase("The Password field is required.")]
  25:      [TestCase("The password and confirmation password do not match.")]
  26:      public void Has_Expected_Validation_ErrorMessages_Test(string message)
  27:      {
  28:          Assert.IsTrue(ActualMessages.Any(x=>x.ErrorMessage== message));
  29:      }
  30:   
  31:      [TestCase("Password")]
  32:      public void Has_Expected__ValidationMessages_Test(string memberName)
  33:      {
  34:          Assert.IsTrue(ActualMessages.Any(x => x.MemberNames.Contains(memberName)));
  35:      }
  36:  }

In this test we made password an empty string and as a result it failed validation, and returned a validation message for password being required and for password and confirm password not matching.

Check out my sample application on Github to see this in action.

No comments: