Testable MVC Models - Part 2: A SOLID Approach

by Larry Spencer Wednesday, April 17, 2013 7:30 PM

Last time, we saw how a naive implementation of even a simple MVC model can be less testable than you may have thought. Now let's use SOLID principles to make it better.

You'll recall that our first implementation of the Contact class had three responsibilities:

  1. Grouping things together as a model.
  2. Determining whether the model is valid.
  3. Knowing what constitutes a valid email address. 

Following the Single Responsibility Principle (the S of SOLID), we will put each of those reponsibilities in a separate class. One could argue that responsibility #2 does belong in the same class as #1, but let's suspend judgment on that until we see the benefit of splitting them up.

If you want to view the code in Visual Studio 2012, it's available for download here: SrpAndTestability.zip (198.97 kb)

 

EmailValidator and Validator<T>

We begin with the EmailValidator class. Having focused closely on the single responsibility of validating an email, it occurs to us that many of our objects will need some sort of validation. We have the bright idea of making an interface, IValidator<T>, that standardizes how this willl work. And we will see more of it!

namespace Interfaces
{
    /// <summary>
    /// Interface for a class that can validate an object of type T.
    /// </summary>
    /// <typeparam name="T">The type of object being validated.</typeparam>
    public interface IValidator<T>
    {
        /// <summary>
        /// Validate an object.
        /// </summary>
        /// <param name="obj">The object to validate.</param>
        /// <param name="exceptionOnError">If true, and the object is invalid,
        /// some sort of Exception will be thrown.</param>
        /// <returns>If the object is valid, return null (meaning no error).
        /// If the object is invalid, returns an error message --
        /// unless <paramref name="exceptionOnError"/>is true, in which case
        /// the error message is thrown as an Exception.</returns>
        /// <remarks>Generally, an object that is empty should pass validation.
        /// If the object is required, check for that separately.</remarks>
        string GetError(T obj, bool exceptionOnError = true);
    }
}

 

using System;
using System.Text.RegularExpressions;
using Interfaces;

namespace BusinessLogicLayer
{
    public class EmailValidator : IValidator<string>
    {
        /// <summary>
        /// Validate that an email address is correctly formatted.
        /// </summary>
        /// <param name="email">The email address.</param>
        /// <param name="exceptionOnError">If true, and the address is invalid,
        /// some sort of Exception will be thrown.</param>
        /// <returns>If the object is valid, return null (meaning no error).
        /// If the object is invalid, returns an error message --
        /// unless <paramref name="exceptionOnError"/>is true, in which case
        /// the error message is thrown as an Exception.</returns>
        /// <remarks>Generally, an object that is empty should pass validation.
        /// If the object is required, check for that separately.</remarks>
        public string GetError(string email, bool exceptionOnError = true)
        {
            if (String.IsNullOrWhiteSpace(email)
            || Regex.IsMatch(email,
                           @"^([0-9a-zA-Z]" + //Start with a digit or letter
                           @"([\+\-_\.][0-9a-zA-Z]+)*" + // No contiguous or ending +-_. chars 
                           @")+" +
                           @"@(([0-9a-zA-Z][-\w]*[0-9a-zA-Z]*\.)+[a-zA-Z0-9]{2,17})$"))
            {
                return null;
            }

            var errorMessage = Properties.Resources.EmailInvalid;
            if (exceptionOnError)
                throw new FormatException(errorMessage);

            return errorMessage;
        }
    }
}

 

You can envision how easy it will be to unit-test EmailValidator. Also, since our attention will be on that one responsibility, our tests are more likely to be thorough. The tests are in the downloadable solution.

 

Contact2

Next up is the new version of the Contact class, Contact2. Look how simple it is! And notice the dependency injection (the D of SOLID) in the constructor. We're injecting our email validator and a validator that you haven't seen yet for the Contact2 class. Both follow the IValidator<T> interface we set up earlier.

using Interfaces;

namespace BusinessLogicLayer
{
    public class Contact2 
    {
        readonly IRepository<Contact2, int> _repository;
        readonly IValidator<Contact2> _validator;

        /// <summary>
        /// The object's key in the repository, or 0 if the object has not 
        /// yet been persisted.
        /// </summary>
        public int ContactId { get; set; }

        public string Name { get; set; }

        public string Email { get; set; }

        public bool IsValid { get { return _validator.GetError(this,false)==null; } }
        
        public Contact2(IRepository<Contact2, int> repository, IValidator<Contact2> validator)
        {
            _repository = repository;
            _validator = validator;
        }

        /// <summary>
        /// Persist the object, but only if it's valid.
        /// </summary>
        /// <returns>The object's ContactId. If saving the object for the first
        /// time, this will be the newly assigned key.</returns>
        /// <exception cref="System.InvalidOperationException">The object is not
        /// in a valid state.</exception>
        public int Save()
        {
            _validator.GetError(this);
            return _repository.Save(this);
        }
    }
}

Now we come to a subtle but important point. Because our unit tests can inject a mock of IValidator<Contact2>, and mocks can track what gets called on them, we can verify that both Save() and IsValid use the injected validator. This ensures consistency without having to duplicate tests as we did in the naive approach.

The beginning of Contact2_UnitTests provides an example. The last line of the unit test validates that the IsValid property called the injected IValidator<Contact2>.

 

[TestClass]
public class Contact2_UnitTests
{     
    Mock<IRepository<Contact2, int>> _mockRepository;
    Mock<IValidator<Contact2>> _mockValidator;
    
    [TestInitialize]
    public void TestInitialize()
    {
        _mockRepository = new Mock<IRepository<Contact2, int>>(MockBehavior.Loose);
        _mockValidator = new Mock<IValidator<Contact2>>(MockBehavior.Strict);
    }
    
    [TestMethod]
    public void IsValid_IfObjectValid_ThenTrue()
    {
        var contact = new Contact2(_mockRepository.Object, _mockValidator.Object);
        _mockValidator.Setup(v => v.GetError(contact, false)).Returns((string)null);
        Assert.IsTrue(contact.IsValid);
        _mockValidator.VerifyAll();
    }

 

Also, the unit tests of Contact2 do not have to know anything about what it means for a Contact2 to be "valid". That's what the Contact2Validator class is for.

 

Contact2Validator

 

public class Contact2Validator : IValidator<Contact2>
{
    readonly IValidator<string> _emailValidator;

    public Contact2Validator(IValidator<string> emailValidator)
    {
        _emailValidator = emailValidator;
    }

    /// <summary>
    /// Validate that an email address is correctly formatted.
    /// </summary>
    /// <param name="email">The email address.</param>
    /// <param name="exceptionOnError">If true, and the address is invalid,
    /// some sort of Exception will be thrown.</param>
    /// <returns>True if the address is valid; 
    /// false if not and <paramref name="exceptionOnError"/>is false.</returns>
    /// <exception cref="System.FormatException">The address is invalid.</exception>
    public string GetError(Contact2 contact, bool exceptionOnError = true)
    {
        string errorMessage = null;
        if (String.IsNullOrWhiteSpace(contact.Name))
            errorMessage = Properties.Resources.NameIsRequired;

        else if (String.IsNullOrWhiteSpace(contact.Email))
            errorMessage = Properties.Resources.EmailIsRequired;

        else
            errorMessage = _emailValidator.GetError(contact.Email, exceptionOnError);

        if (errorMessage == null)
            return null;
        if (exceptionOnError)
            throw new InvalidOperationException(errorMessage);
        return errorMessage;
    }
}

Again we have adhered to the Single Responsibility Principle. This class's sole responsibility is to validate the contact qua contact. In particular, it does not validate the email address; the injected email validator takes care of that. In the unit tests we can inject a validator that passes or fails the email address as the test requires.

Incidentally, if the need ever arose to employ the same Contact2 data structure, but validate it differently (e.g., by not requiring the email address), we could inject a different validator.

 

Conclusion

By scrupulously following the Single Responsibility Principle and using Dependency Injection, we have avoided testing the same conditions in the contact's IsValid, Save and GetError members, while ensuring that all those members use the same validation code.

We have promoted code reuse by extracting email validation into a separate class.

Finally, we have left the door open for using different, injected validators in the future.

Tags: , , , , ,

All | ASP.NET MVC | Dependency Injection

About the Author

Larry Spencer

Larry Spencer develops software with the Microsoft .NET Framework for ScerIS, a document-management company in Sudbury, MA.