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 Senior Software Developer for Red Cedar Solutions Group in Okemos, Michigan. 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 [email protected].

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube