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

  • AI for GitHub Collaboration? Maybe Not So Much

    No doubt GitHub Copilot has been a boon for developers, but AI might not be the best tool for collaboration, according to developers weighing in on a recent social media post from the GitHub team.

  • Visual Studio 2022 Getting VS Code 'Command Palette' Equivalent

    As any Visual Studio Code user knows, the editor's command palette is a powerful tool for getting things done quickly, without having to navigate through menus and dialogs. Now, we learn how an equivalent is coming for Microsoft's flagship Visual Studio IDE, invoked by the same familiar Ctrl+Shift+P keyboard shortcut.

  • .NET 9 Preview 3: 'I've Been Waiting 9 Years for This API!'

    Microsoft's third preview of .NET 9 sees a lot of minor tweaks and fixes with no earth-shaking new functionality, but little things can be important to individual developers.

  • Data Anomaly Detection Using a Neural Autoencoder with C#

    Dr. James McCaffrey of Microsoft Research tackles the process of examining a set of source data to find data items that are different in some way from the majority of the source items.

  • What's New for Python, Java in Visual Studio Code

    Microsoft announced March 2024 updates to its Python and Java extensions for Visual Studio Code, the open source-based, cross-platform code editor that has repeatedly been named the No. 1 tool in major development surveys.

Subscribe on YouTube