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

  • Hands On: New VS Code Insiders Build Creates Web Page from Image in Seconds

    New Vision support with GitHub Copilot in the latest Visual Studio Code Insiders build takes a user-supplied mockup image and creates a web page from it in seconds, handling all the HTML and CSS.

  • Naive Bayes Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the naive Bayes regression technique, where the goal is to predict a single numeric value. Compared to other machine learning regression techniques, naive Bayes regression is usually less accurate, but is simple, easy to implement and customize, works on both large and small datasets, is highly interpretable, and doesn't require tuning any hyperparameters.

  • VS Code Copilot Previews New GPT-4o AI Code Completion Model

    The 4o upgrade includes additional training on more than 275,000 high-quality public repositories in over 30 popular programming languages, said Microsoft-owned GitHub, which created the original "AI pair programmer" years ago.

  • Microsoft's Rust Embrace Continues with Azure SDK Beta

    "Rust's strong type system and ownership model help prevent common programming errors such as null pointer dereferencing and buffer overflows, leading to more secure and stable code."

  • Xcode IDE from Microsoft Archrival Apple Gets Copilot AI

    Just after expanding the reach of its Copilot AI coding assistant to the open-source Eclipse IDE, Microsoft showcased how it's going even further, providing details about a preview version for the Xcode IDE from archrival Apple.

Subscribe on YouTube

Upcoming Training Events