C# Corner

TDD for ASP.NET MVC Part 4: Unit Testing View Model Validation

How to unit test view model validation, focusing on the controller when the model is bound to a controller action.

Welcome to part 4 in the TDD for ASP.NET MVC series. This time, I'll cover how to unit test view models in ASP.NETMVC. I'll focus primarily on how to unit test the model state validation that is performed by ASP.NET MVC in the controller when the model is bound to a controller action.

To get started download and open up the solution for part 3. Next open up the Tests project and add a new folder to the project named ViewModels. Then create a new class file in the ViewModels folder called TestModelHelper. Next, add the following using statements to the TestModelHelper class file:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

Then I add the Validate method, which performs validation on the given model object.

First I create a new validation result collection and validation context:

var results = new List<ValidationResult> ();
var validationContext = new ValidationContext(model, null, null); 

Then I perform the standard model binder validation on the model, storing the results in the results object:

Validator.TryValidateObject(model, validationContext, results, true); 

Next I perform the IValidatableObject validation on the model if it implements the interface:

if (model is IValidatableObject) (model as IValidatableObject).Validate(validationContext); 

Then I return the validation results:

return results;

See Listing 1 for the completed TestModelHelper class.

Listing 1: TestModelHelper.cs Class File

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace VSMMvcTDD.Tests.ViewModels
{
    public class TestModelHelper
    {
        public static IList<ValidationResult> Validate(object model)
        {
            var results = new List<ValidationResult>();
            var validationContext = new ValidationContext(model, null, null);
            Validator.TryValidateObject(model, validationContext, results, true);
            if (model is IValidatableObject) (model as IValidatableObject).Validate(validationContext);
            return results;
        }
    }
}

Now it's time to unit test the ContactViewModel class to the ViewModels folder in the Tests project. I will be updating the ContactViewModel to enforce that the LastName property is required, that the FirstName and LastName properties cannot exceed 100 characters, and the Email property cannot exceed 255 characters.

First I add a unit test for a valid model, which has a last name set:

[TestMethod]
public void Validate_Model_Given_Valid_Model_ExpectNoValidationErrors()
{
    var model = new ContactViewModel()
    {
        LastName = "Vogel"
    };

    var results = TestModelHelper.Validate(model);

    Assert.AreEqual(0, results.Count);
}

The test should pass for now as there has been no validation added to the ContactViewModel yet. Next I add a unit test to test for an invalid model that has a null LastName property:

[TestMethod]
public void Validate_Model_Given_LastName_Is_Null_ExpectOneValidationError()
{
    var model = new ContactViewModel();

    var results = TestModelHelper.Validate(model);

    Assert.AreEqual(1, results.Count);
    Assert.AreEqual("The Last Name field is required.", results[0].ErrorMessage);
}

The unit test should fail for now because the LastName property isn't required yet. Open up the ContactViewModel class and add a Required attribute to the LastName property to make the unit test pass:

[Required]
public String LastName { get; set; }
Next I add a unit test for a contact with a first name over 100 characters, which will fail:
[TestMethod]
public void Validate_Model_Given_FirstName_Exceeds_100_Characters_ExpectError()
{
    var model = new ContactViewModel()
    {
        LastName = "Vogel",
        FirstName = new string('*', 101)
    };

    var results = TestModelHelper.Validate(model);

    Assert.AreEqual(1, results.Count);
}

Then I make the unit test pass by adding a StringLength attribute to the FirstName property with a value of 100:

[Display(Name = "First Name")]
[StringLength(100)]
public String FirstName { get; set; }
Next I add a unit test to make sure the last name cannot exceed 100 characters also:
[TestMethod]
 public void Validate_Model_Given_LastName_Exceeds_100_Characters_ExpectError()
 {
     var model = new ContactViewModel()
     {
         LastName = new string('*', 101)
     };

     var results = TestModelHelper.Validate(model);

     Assert.AreEqual(1, results.Count);
 }

Then I make the test pass by adding a StringLength attribute to the LastName property with a value of 100 too:

[Display(Name = "First Name")]
[StringLength(100)]
public String FirstName { get; set; }

Next I add a unit test to verify that the contact's email cannot exceed 255 characters:

[TestMethod]
public void Validate_Model_Given_Email_Exceeds_255_Characters_ExpectError()
{
    var model = new ContactViewModel()
    {
        LastName = "Vogel",
        Email = new string('*', 256)
    };

    var results = TestModelHelper.Validate(model);

    Assert.AreEqual(1, results.Count);
}

Then I make the unit test pass by adding a StringLength attribute to the Email property with a value of 255:

[StringLength(255)]
public String Email { get; set; }

Your completed ContactViewModel class should now look like Listing 2.

Listing 2: Updated ContactViewModel with Validation

using System;
using System.ComponentModel.DataAnnotations;

namespace VSMMvcTDD.Models
{
    public class ContactViewModel : IEquatable<ContactViewModel>
    {
        public bool Equals(ContactViewModel other)
        {
            if (ReferenceEquals(null, other)) return false;
            if (ReferenceEquals(this, other)) return true;
            return Id == other.Id && string.Equals(FirstName, other.FirstName) && string.Equals(LastName, other.LastName) && string.Equals(Email, other.Email);
        }

        public override int GetHashCode()
        {
            unchecked
            {
                int hashCode = Id;
                hashCode = (hashCode * 397) ^ (FirstName != null ? FirstName.GetHashCode() : 0);
                hashCode = (hashCode * 397) ^ (LastName != null ? LastName.GetHashCode() : 0);
                hashCode = (hashCode * 397) ^ (Email != null ? Email.GetHashCode() : 0);
                return hashCode;
            }
        }

        public static bool operator ==(ContactViewModel left, ContactViewModel right)
        {
            return Equals(left, right);
        }

        public static bool operator !=(ContactViewModel left, ContactViewModel right)
        {
            return !Equals(left, right);
        }

        public int Id { get; set; }

        [Display(Name = "First Name")]
        [StringLength(100)]
        public String FirstName { get; set; }

        [Display(Name = "Last Name")]

        [Required]
        [StringLength(100)]
        public String LastName { get; set; }

        [StringLength(255)]
        public String Email { get; set; }

        public override bool Equals(object obj)
        {
            if (ReferenceEquals(null, obj)) return false;
            if (ReferenceEquals(this, obj)) return true;
            if (obj.GetType() != this.GetType()) return false;
            return Equals((ContactViewModel)obj);
        }
    }
}

Your completed ContactViewModel test file should now look like Listing 3.

Listing 3: Completed ContactVideModelTests Class

using Microsoft.VisualStudio.TestTools.UnitTesting;
using VSMMvcTDD.Models;

namespace VSMMvcTDD.Tests.ViewModels
{
    [TestClass]
    public class ContactViewModelTests
    {
        [TestMethod]
        public void Validate_Model_Given_Valid_Model_ExpectNoValidationErrors()
        {
            var model = new ContactViewModel()
            {
                LastName = "Vogel"
            };

            var results = TestModelHelper.Validate(model);

            Assert.AreEqual(0, results.Count);
        }

        [TestMethod]
        public void Validate_Model_Given_LastName_Is_Null_ExpectOneValidationError()
        {
            var model = new ContactViewModel();

            var results = TestModelHelper.Validate(model);

            Assert.AreEqual(1, results.Count);
            Assert.AreEqual("The Last Name field is required.", results[0].ErrorMessage);
        }

        [TestMethod]
        public void Validate_Model_Given_FirstName_Exceeds_100_Characters_ExpectError()
        {
            var model = new ContactViewModel()
            {
                LastName = "Vogel",
                FirstName = new string('*', 101)
            };

            var results = TestModelHelper.Validate(model);

            Assert.AreEqual(1, results.Count);
        }

        [TestMethod]
        public void Validate_Model_Given_LastName_Exceeds_100_Characters_ExpectError()
        {
            var model = new ContactViewModel()
            {
                LastName = new string('*', 101)
            };

            var results = TestModelHelper.Validate(model);

            Assert.AreEqual(1, results.Count);
        }

        [TestMethod]
        public void Validate_Model_Given_Email_Exceeds_255_Characters_ExpectError()
        {
            var model = new ContactViewModel()
            {
                LastName = "Vogel",
                Email = new string('*', 256)
            };

            var results = TestModelHelper.Validate(model);

            Assert.AreEqual(1, results.Count);
        }
    }
}

As you can see it is fairly easy to unit test view model validation in an ASP.NET MVC project. You can now safely add new input validation business requirements knowing that the old tests with get regression tested with each test execution.

Stay tuned for the final installment in the series, where I'll finish the view and JavaScript implementation.

About the Author

Eric Vogel is a Sr. Software Developer at Kunz, Leigh, & Associates in Okemos, MI. He is the president of the Greater Lansing User Group for .NET. Eric enjoys learning about software architecture and craftsmanship, and is always looking for ways to create more robust and testable applications. Contact him at vogelvision@gmail.com.

comments powered by Disqus
Upcoming Events

.NET Insight

Sign up for our newsletter.

I agree to this site's Privacy Policy.