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

  • What's Next for ASP.NET Core and Blazor

    Since its inception as an intriguing experiment in leveraging WebAssembly to enable dynamic web development with C#, Blazor has evolved into a mature, fully featured framework. Integral to the ASP.NET Core ecosystem, Blazor offers developers a unique combination of server-side rendering and rich client-side interactivity.

  • Nearest Centroid Classification for Numeric Data Using C#

    Here's a complete end-to-end demo of what Dr. James McCaffrey of Microsoft Research says is arguably the simplest possible classification technique.

  • .NET MAUI in VS Code Goes GA

    Visual Studio Code's .NET MAUI workload, which evolves the former Xamarin.Forms mobile-centric framework by adding support for creating desktop applications, has reached general availability.

  • Visual Studio Devs Quick to Sound Off on Automatic Updates: 'Please No'

    A five-year-old Visual Studio feature request for automatic IDE updates is finally getting enacted by Microsoft amid a lot of initial developer pushback, seemingly misplaced.

  • First Official OpenAI Library for .NET Goes Beta

    Although it seems Microsoft and OpenAI have been deeply intertwined partners for a long time, they are only now getting around to releasing an official OpenAI library for .NET developers, joining existing community libraries.

Subscribe on YouTube