Tuesday, 25 August 2009

Validation Framework with MVC Support (Part 2) - Tests

Following on from the previous post describing a validation framework I'm planning to use on MVC projects, this post extends the development to incorporate a range of tests.

To recap, I've set up a domain model structure that consists of:
  • Entities - simple classes representing the fields and relationships of my business objects. They consist of properties and methods that act only on those properties (i.e. they have no external dependencies.
  • Repository layer - a number of data access methods that provide CRUD methods for retrieving and data from the database and instantiating the entities, and persisting their changes back.
  • Service layer - a layer consisting of methods that sit between the UI and repository layer, passing method calls and objects in between.
These classes combine to provide validation at three levels:
  • Entity validation - these are C# methods defined on the entity object itself to internally validate its fields. They will consist of checks for required fields, range checking and internal field comparisons.
  • Service validation - these are checks made against external dependencies. Examples might be user authorisation or arbitrary business rules such as "customer records can only be saved on a Friday".
  • Database validation - basically wrappers around database exceptions, such as errors caused due to deleting records where foreign key constraints are violated, or entry of duplicate key values.
In order to test the classes created for each entity type, I've set up four folders, each for different types of tests:
  • Entity tests - unit tests on the entity self-validation rules
  • Service tests - unit tests on the service layer, using a mocked repository
  • Controller tests - unit tests on the MVC controller class, using a mocked service layer
  • Integration tests - tests that hit the database, running against a known set of data
An example of each follows.

Customer Entity Test

This test prepares a customer object with a known validation error (a missing name). The rule violations are queried and asserted to check there is a single correct exception reported.


    1 private Customer PrepareCustomer()


    2 {


    3     return new Customer


    4     {


    5         Id = 1,


    6         Name = "Test Customer"


    7     };


    8 }


    9 


   10 [Description("Entity Test"), TestMethod]


   11 public void Customer_InvalidCustomerWithMissingName_GeneratesException()


   12 {


   13     //Arrange


   14     Customer customer = PrepareCustomer();


   15 


   16     //Act


   17     customer.Name = "";


   18 


   19     //Assert


   20     Assert.AreEqual(1, customer.GetRuleViolations().Count);


   21     Assert.AreEqual("Name is a required field.", customer.GetRuleViolations()[0]);


   22 }




Customer Service Test

This unit test acts on a method in the service layer. The idea here is to isolate the function and test it without dependencies - i.e. by mocking the repository layer to the test doesn't actually hit the database. In other words, by mocking the repository, we can say given a defined behaviour in the repository method, does the service method respond appropriately. Here we are checking that the correct exceptions are thrown given and valid and invalid Customer object.


    1 private CustomersService service = new CustomersService(MockCustomersRepository());


    2 


    3 static ICustomersRepository MockCustomersRepository()


    4 {


    5     //Set up test data


    6     IList<Customer> customers = new List<Customer>


    7             {


    8                 new Customer {Id = 2, Name = "Drama"},


    9                 new Customer {Id = 1, Name = "Entertainment"}


   10             };


   11 


   12     // Generate an implementor of IProductsRepository at runtime using Moq


   13     var mockCustomersRepository = new Moq.Mock<ICustomersRepository>();


   14     mockCustomersRepository.Setup(x => x.UpdateCustomer(It.IsAny<Customer>())).Verifiable();


   15 


   16     return mockCustomersRepository.Object;


   17 }


   18 


   19 private Customer PrepareCustomer()


   20 {


   21     return new Customer


   22     {


   23         Id = 1,


   24         Name = "Test Customer"


   25     };


   26 }


   27 


   28 [Description("Service Test"), TestMethod]


   29 public void CustomerService_UpdateValidCustomerWithValidAccount_GeneratesNoExceptions()


   30 {


   31     // Arrange


   32     Customer customer = PrepareCustomer();


   33     User user = new User { Id = 1, Role = new Role { Id = 1, Name = "Administrator" } };


   34 


   35     // Act: Request the update method for customer that will pass validation and with valid account


   36     service.UpdateCustomer(customer, user);


   37 


   38     // Assert: Check the results


   39 }


   40 


   41 [Description("Service Test"), TestMethod]


   42 public void CustomerService_UpdateCustomerWithMissingName_GeneratesRuleExceptions()


   43 {


   44     // Arrange


   45     Customer customer = PrepareCustomer();


   46     customer.Name = "";


   47     User user = new User { Id = 1, Role = new Role { Id = 1, Name = "Administrator" } };


   48 


   49     // Act: Request the update method for customer that will fail validation due to missing name


   50     bool ok = false;


   51     try


   52     {


   53         service.UpdateCustomer(customer, user);


   54         ok = true;


   55     }


   56     catch (RuleException ex)


   57     {


   58         // Assert: Check the results


   59         Assert.AreEqual(1, ex.Errors.Count);


   60         Assert.AreEqual("Name is a required field.", ex.Errors[0]);


   61     }


   62     Assert.IsFalse(ok);


   63 }




Customer Integration Test

This test involves the database, and confirms that the correct information is written to and retrieved from it.


    1 [Description("Integration Test"), TestMethod]


    2 public void CustomerIntegration_UpdateValidCustomerWithValidAccount_UpdatesCustomerDetails()


    3 {


    4     //Arrange           


    5     Customer customer = service.GetCustomer(1);


    6     User user = new User { Id = 1, Role = new Role { Id = 1, Name = "Administrator" } };


    7 


    8     //Act           


    9     customer.Name = "Test Customer Changed";


   10     service.UpdateCustomer(customer, user);


   11     customer = service.GetCustomer(1);


   12 


   13     //Assert


   14     Assert.AreEqual("Test Customer Changed", customer.Name);


   15 


   16     //Revert


   17     customer.Name = "Test Customer";


   18     service.UpdateCustomer(customer, user);


   19     customer = service.GetCustomer(1);


   20     Assert.AreEqual(1, customer.Id);


   21     Assert.AreEqual("Test Customer", customer.Name);


   22 }




Customer Controller Test

In this test we are mocking the service layer, and testing the behaviour of the controller. Again we can say that given a mocked behaviour of the service layer, do the controller methods respond with the appropriate ActionResults (redirects or view rendering).


    1 private ICustomersService CustomersService = MockCustomersService();


    2 


    3 static ICustomersService MockCustomersService()


    4 {


    5     //Set up test data


    6     IList<customer> customers = new List<customer>


    7     {


    8         new customer {Id = 1, Name = "Test Customer"},


    9         new customer {Id = 2, Name = "Test Customer Two"}


   10     };


   11 


   12     // Generate an implementor of ICustomersService at runtime using Moq


   13     var mockCustomersService = new Moq.Mock<ICustomersService>();


   14     mockCustomersService.Setup(x => x.GetCustomer(1).Returns(Customers[0]);


   15     mockCustomersService.Setup(x => x.UpdateCustomer(It.Is<Customer>(t => t.Name == "Invalid"), null)).Throws(new RuleException(new NameValueCollection { { "Name", "Invalid" } }));


   16     return mockCustomersService.Object;


   17 }


   18 


   19 [Description("Controller Test"), TestMethod]


   20 public void CustomersController_EditValidCustomer_ReturnsCorrectRedirectAction()


   21 {


   22     // Arrange: get controller


   23     CustomersController controller = new CustomersController(CustomersService, usersService);


   24     ContextMocks mocks = new ContextMocks(controller);


   25 


   26     // Act: Request the edit action for Customer that will pass validation


   27     var result = controller.Edit(new Customer { Id = 3, Name = "Valid" }, null, null);


   28 


   29     // Assert: Check the results


   30     Assert.IsNotNull(result);


   31     Assert.IsInstanceOfType(result, typeof(RedirectToRouteResult));


   32     Assert.AreEqual("Index", ((RedirectToRouteResult)result).RouteValues["action"]);


   33 }


   34 


   35 [Description("Controller Test"), TestMethod]


   36 public void CustomersController_EditInvalidCustomer_ReturnsViewWithErrorsInModelState()


   37 {


   38     // Arrange: get controller


   39     CustomersController controller = new CustomersController(CustomersService, usersService);


   40     ContextMocks mocks = new ContextMocks(controller);


   41 


   42     // Act: Request the edit action for Customer that will fail validation          


   43     ActionResult result = null;


   44     Customer Customer = CustomersService.GetCustomer(3, CustomerLoad.LoadCustomerOnly);


   45     Customer.Name = "Invalid";


   46     result = controller.Edit(Customer, null, null);


   47 


   48     // Assert: Check the results


   49     Assert.IsNotNull(result);


   50     Assert.IsInstanceOfType(result, typeof(ViewResult));


   51     Assert.AreEqual("Edit", ((ViewResult)result).ViewName);


   52     Assert.IsFalse(controller.ModelState.IsValid);


   53     Assert.AreEqual(1, controller.ModelState.Count);


   54 }




Conclusion

As you can see, this is a very simple, yet fairly comprehensive set of tests on the domain model. Although there's a fair bit of work involved for each set of classes, the patterns are straightforward and hence should be relatively easy for a developer or team to adhere to. Following it I hope is going to lead to a robust domain model, with a consistent and reliable means of validating business rules.

One last note on the integration tests. As they involve hitting the database, they aren't classed as unit tests - they suffer from external dependencies that may lead to brittleness, and they will be a little slower to run due to the need to connect for real to the database. They are therefore a bit more painful to maintain - they can quickly become out of synch with the data, leading to false negative results, and untrustworthy tests.

It's going to take a bit of discipline to maintain this I'm sure - but the way I'm looking to do this is that once I had the bulk of my database structure built, I used the database publishing wizard to generate a script of the full schema and data. This I amended slightly to add statements to drop and create the database, and then checked the file into the solution.

From now on, following any updates to the database schema, I must also make the change to the script. Doing this means that when I come to run the integration tests that are coded to expect particular data values in return, I just need to run the script before running the tests.

Sunday, 9 August 2009

Validation Framework with MVC support

The most recent project I've started to work on is going to require a number of web front-ends for a business. There will be a single database, from which information will be drawn and written to in various ways on the different websites. This situation suggests a robust means of applying and enforcing business rules and validation within a common business logic layer; promoting code re-use across the sites and avoiding the maintenance headache of having rules in the UI layer.

Due to a delayed start of the project, I've had a bit of time to investigate some different options to solve this - and to have a hard look at some of the techniques that have been used in the past where, if I'm honest, this wasn't always best achieved.

CSLA.Net

Firstly, I spent a bit of time with CSLA.Net. This is a robust, tried-and-tested framework for implementing your business objects. It's very powerful and I'm sure would prove a good solution. However in the end I decided against it - for two reasons.

The first was that it was proving tricky to use in a testable manner. It could well be that with perseverance I could have got around this, but it seems that the architecture employed (static methods, encapsulated business and data access) made separation of concerns and mocking/unit testing difficult.

Second was really just a nagging feeling that a lot of the features I would never be likely to use - particular those geared to Windows Forms. The mechanism supports n-level undo, remoting objects and the retrieval and saving of large object graphs - none of which I could see a likely scenario that I would require in the medium term.

Validation Framework

One great thing CSLA.Net would have provided though was a consistent means of handling business object validation - and so this is what I looked to replicate in the solution.

Much of the credit here really needs to go to the author of the recently released ASP.Net MVC Framework book from APress. I can certainly recommend this book highly - it does a great job of explaining not only the MVC framework, but also the patterns techniques deployed in the domain model layer, namely repositories and inversion of control/dependency injection.

The crux of the validation framework I've employed on this project, and no doubt on an ongoing basis, is that a particular business object or entity (e.g. a Customer object) may be validated agaist three groups of criteria:
  1. Self-validation. This would be anything that the simply entity class can validate about itself (e.g. required fields, ranges of valid values, internal field comparisons)
  2. External validation. This would be business rules enforced based on factors outside of the immediate properties and methods of the class (e.g. user authorisations)
  3. Database validation. These are really a special class of external validation that is enforced in the database (e.g. dependent records for deletions, unique fields).
Code wise my domain model layer is implemented as a single C# class library (though some might choose to split into three projects, one for each component).

The business objects or entities are deliberately implemented as simply as possible. They only contain properties, methods that can act on internal fields only (e.g. an Order class that calculated it's own total would be a good candidate), and methods to implement an interface I've set up to enforce consistent self-validation:


    1 using System;


    2 using System.Collections.Specialized;


    3 


    4 namespace DomainModel.Interfaces


    5 {


    6     /// <summary>


    7     /// Interface for all entity classes that perform self-validation


    8     /// </summary>


    9     interface IValidatingEntity


   10     {


   11         NameValueCollection GetRuleViolations();


   12     }


   13 }




    1 using System;


    2 using System.Collections.Specialized;


    3 using DomainModel.Interfaces;


    4 


    5 namespace DomainModel.Entities


    6 {


    7     /// <summary>


    8     /// Class representiting a Title entity and it's self-validation rules


    9     /// </summary>


   10     public class Customer : IValidatingEntity


   11     {


   12         public int Id { get; set; }


   13         public string Name { get; set; }


   14 


   15         /// <summary>


   16         /// Checks class internal members are valid


   17         /// </summary>


   18         /// <returns>Dictionary of any valuations (property/message)</returns>


   19         public NameValueCollection GetRuleViolations()


   20         {


   21             NameValueCollection errors = new NameValueCollection();


   22 


   23             if (string.IsNullOrEmpty(Name))


   24                 errors.Add("Name", "Name is a required field.");


   25             else


   26                 if (Name.Length > 100)


   27                     errors.Add("Name", "The Name field must be 100 characters or less.");


   28 


   29             return errors;


   30         }


   31     }


   32 }




My data access layer is implemented using repositories for each area of the application - in this case a Customer repository that implements an interface:


    1 using System;


    2 using DomainModel.Entities;


    3 


    4 namespace DomainModel.Repository.Abstract


    5 {


    6     /// <summary>


    7     /// Interface for Customers repository


    8     /// </summary>


    9     public interface ICustomersRepository


   10     {


   11         IList<Customer> GetCustomerList();


   12         Customer GetCustomer(int id);


   13         void UpdateCustomer(Customer title);


   14     }


   15 }




    1 using System;


    2 using System.Data;


    3 using System.Data.SqlClient;


    4 using DomainModel.Entities;


    5 using DomainModel.Repository.Abstract;


    6 


    7 namespace DomainModel.Repository.Concrete


    8 {


    9     /// <summary>


   10     /// Customers repository implemented using SQL ADO.Net data access methods


   11     /// </summary>


   12     public class SqlCustomersRepository : ICustomersRepository


   13     {


   14         private string connectionString;


   15 


   16         public SqlCustomersRepository(string connectionString)


   17         {


   18             this.connectionString = connectionString;


   19         }       


   20 


   21         /// <summary>


   22         /// Persists a Customer object to the database


   23         /// </summary>


   24         /// <param name="customer">Customer object</param>


   25         public void UpdateCustomer(Customer customer)


   26         {


   27             using (SqlConnection cn = new SqlConnection(connectionString))


   28             {


   29                 SqlCommand cm = cn.CreateCommand();


   30                 cm.CommandType = CommandType.StoredProcedure;


   31                 cm.CommandText = "updateCusomter";


   32                 cm.Parameters.AddWithValue("@id", customer.Id);


   33                 cm.Parameters.AddWithValue("@name", customer.Name);


   34                 cn.Open();


   35                 cm.ExecuteNonQuery();


   36                 cn.Close();


   37             }


   38         }


   39     }


   40 }




Finally, acting as the bridge between the repository and the UI layer sits a service layer:


    1 using System;


    2 using DomainModel.Entities;


    3 


    4 namespace DomainModel.Service.Abstract


    5 {


    6     /// <summary>


    7     /// Interface for Customers service


    8     /// </summary>


    9     public interface ITitlesService


   10     {


   11         IList<Customer> GetCustomerList();


   12         Customer GetCustomer(int id);


   13         void UpdateCustomer(Customer title);


   14     }


   15 }




    1 using System;


    2 using System.Collections.Generic;


    3 using System.Linq;


    4 using System.Text;


    5 using System.Collections.Specialized;


    6 using DomainModel.Entities;


    7 using DomainModel.Service.Abstract;


    8 using DomainModel.Repository.Abstract;


    9 using DomainModel.Exceptions;


   10 using DomainModel.Interfaces;


   11 


   12 namespace DomainModel.Service.Concrete


   13 {


   14     /// <summary>


   15     /// Customers service layer


   16     /// </summary>


   17     public class CustomersService : ICustomersService


   18     {


   19         private ICustomersRepository repository;


   20 


   21         public CustomersService(ICustomersRepository repository)


   22         {


   23             this.repository = repository;


   24         }


   25 


   47         /// <summary>


   48         /// Validates (at entity, service and database level) and saves a Customer object


   49         /// </summary>


   50         /// <param name="customer">Customer object</param>


   51         /// <param name="user">User making the update</param>


   52         public void UpdateCustomer(Customer customer, User user)


   53         {


   54             NameValueCollection errors = customer.GetRuleViolations();


   55             GetServiceViolations(ref errors, user);


   56             if (errors.Count > 0)


   57                 throw new RuleException(errors);


   58             try


   59             {


   60                 repository.UpdateCustomer(customer);


   61             }


   62             catch (Exception ex)


   63             {


   64                 if (GetDatabaseViolations(ex, ref errors))


   65                     throw new RuleException(errors);


   66                 else


   67                     throw (ex);     // Throw original error if not handled


   68             }           


   69         }


   70 


   71         /// <summary>


   72         /// Checks any service level business rules


   73         /// </summary>


   74         /// <param name="errors">Existing collection of errors</param>


   75         private void GetServiceViolations(ref NameValueCollection errors, User user)


   76         {


   77             if(!user.IsInRole("Edit"))


   78                 errors.Add("Authorisation", "Your account is not authorised to perform this action.");


   79         }


   80 


   81         /// <summary>


   82         /// Handles a database exception triggered after a save.


   83         /// </summary>


   84         /// <param name="ex">Exception triggered by database save</param>


   85         /// <param name="errors">Existing collection of errors</param>


   86         /// <returns>Flag for if any handled database exceptions found</returns>


   87         private bool GetDatabaseViolations(Exception ex, ref NameValueCollection errors)


   88         {


   89             bool handledError = false;


   90             if (ex.Message.IndexOf("UIX_Customers_Name") >= 0)


   91             {


   92                 errors.Add("Database Constraint", "Names of Customers must be unique.");


   93                 handledError = true;


   94             }


   95             return handledError;


   96         }


   97     }




Which can be then brought into the UI layer and represented in the by copying them to the ModelStateDictionary, and hence hooking up with the MVC Framework's standard means of displaying validation errors in views. This last step is best implemented as an extension method in the UI layer, to avoid fixing a dependency on MVC in the domain model.


    1 using System;


    2 using System.Collections.Generic;


    3 using System.Web;


    4 using System.Web.Mvc;


    5 using DomainModel.Exceptions;


    6 


    7 namespace WebUI.ExtensionMethods


    8 {


    9     /// <summary>


   10     /// MVC project extension methods


   11     /// </summary>


   12     public static class ExtensionMethods


   13     {


   14         /// <summary>


   15         /// Extends RuleException (defined in domain model) with a method to copy it's contents to the MVC


   16         /// ModelStateDictionary


   17         /// </summary>


   18         /// <param name="ex">Rule exception object to apply extension to</param>


   19         /// <param name="modelState">MVC model state dictionary</param>


   20         /// <remarks>


   21         /// Implemented in web project to avoid MVC dependency in domain model layer.


   22         /// </remarks>


   23         public static void CopyToModelState(this RuleException ex, ModelStateDictionary modelState)


   24         {


   25             foreach (string key in ex.Errors)


   26                 foreach (string value in ex.Errors.GetValues(key))


   27                     modelState.AddModelError(key, value);


   28         }


   29     }


   30 }




The resulting controller action, that calls the validation and save of the Customer object looks like this:


   42         [AcceptVerbs(HttpVerbs.Post)]


   43         public ActionResult Edit(Customer title)


   44         {


   45             if (ModelState.IsValid)


   46             {


   47                 try


   48                 {


   49                     titlesService.UpdateCustomer(title);


   50                 }


   51                 catch (RuleException ex)


   52                 {


   53                     ex.CopyToModelState(ModelState);


   54                 }


   55             }


   56             if (ModelState.IsValid)


   57                 return RedirectToAction("List");


   58             else


   59                 return View("Edit", title);


   60         }




In summary, I think this going to prove a simple but easily adhered to and and clear pattern for centralised business rules. Will post further once it has been put through its paces in a real application.