Wednesday, September 15, 2010

Fluent Validation – Part I

In the introduction article of this series, we looked at a very basic example of how to use the Fluent Validation framework. Moving forward, we will be following a Test driven development (TDD) approach to creating and implementing our validation rules and classes (I am assuming you some have knowledge in this area, as this series is not about TDD. There are plenty of great resources on the web for this.)

So let’s get started by firing up Visual Studio, and creating a blank solution called FluentValidation.Example.Solution. We will add two projects to our solution, FluentValidation.Example.Models, and FluentValidation.Example.UnitTests (for unit testing I will be using the MSTest tool, but feel free to use NUnit or any other testing framework). Also, add a reference to the Models project in our UnitTests project and a reference to the FluentValidation.dll in our UnitTests and Modes projects (this can be downloaded here. I am using the 1.3 version).

Add a new class to your UnitTests project called ValidatorExtensions (you may need to add using statements for FluentValidation.Validators and FluentValidation.Results to get this code to compile):

public static class ValidatorExtensions
{
public static void ContainsRuleFor<T>(this IValidator validator, Expression<Func<T, object>> propertyExpression)
{
string propertyToTest = ((MemberExpression)propertyExpression.Body).Member.Name;
ContainsRuleFor(validator, propertyToTest);
}

public static void ContainsRuleFor(this IValidator validator, string propertyToTest)
{
var descriptor = validator.CreateDescriptor();
var validationRules = descriptor.GetValidatorsForMember(propertyToTest);
List<IPropertyValidator> listToTest = new List<IPropertyValidator>(validationRules);
Assert.IsTrue(listToTest.Count > 0, "No validation rules have been defined for " + propertyToTest);
}

public static void AssertValidationFails(this ValidationResult results, string errorMessage)
{
Assert.IsFalse(results.IsValid, "Object is valid");
Assert.IsTrue(results.Errors.Count > 0, "No validation errors were reported");
Assert.AreEqual(errorMessage, results.Errors[0].ErrorMessage);
}

public static void AssertValidationPasses(this ValidationResult results)
{
Assert.IsTrue(results.IsValid, "Object is not valid");
Assert.AreEqual(0, results.Errors.Count, "Validation errors were reported");
}
}

This may look like a lot of code, and you may not understand some of it right now. That’s ok. As we proceed, it will start to make more sense. Our first two extension methods are called ContainsRuleFor. These methods interrogate our validator class looking to see if any validation rules exist for a given property. This is important, because our unit tests will incorrectly pass when no rules exist, which makes sense. If there is no rule to break, the test passes. Since we are going to follow a test-driven approach, we want our tests to fail until we have implemented the validation rules and these methods help facilitate that behavior. Our next two extension methods, AssertValidationFails and AssertValidationPasses test the results of our validation and check to see if the object is valid when it should be. For this example, we are going to create a simple Employee class and add some apply some validation rules to it. Add a new class to the Models project called Employee:

public class Employee
{
public int ID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}

Our business requirements state that an employee must have a first and last name, and the maximum number of characters is fifty. We also don’t want any numbers or special characters in our names so we will limit the input to the ABCs, a dash, a space, and a period. With these rules in mind, it is time to write our first test. Add a new class to the UnitTests project called EmployeeValidatorTests.

[TestClass]
public class EmployeeValidatorTests
{
private Employee target;
private IValidator<Employee> validator;

[TestInitialize]
public void Init()
{
//arrange
target = new Employee();
validator = new EmployeeValidator();
}

[TestMethod]
public void FirstName_OnInitialize_ValidationFails()
{
//act
ValidationResult result = validator.Validate(target, x => x.FirstName);

//assert
validator.ContainsRuleFor<Employee>(x => x.FirstName);
result.AssertValidationFails("First name is required.");
}
}

At this point, our code will not compile until we’ve added a few things. Add using statements for FluentValidation, FluentValidation.Results, and to our Models project namespace. Next we need to create an EmployeeValidator class. Add a new class name EmployeeValidator to the Models project.

public class EmployeeValidator : AbstractValidator<Employee>
{
}

Notice that it extends the AbstractValidator base class provided by the FluentValidation framework. This is how all of our validation classes will begin. If we run this test now, it will fail – which is the result we are looking for (this is in line with the TDD mantra “red-green-refactor”). Now, let’s implement our first validation rule. Add the following to the EmployeeValidator class:

public EmployeeValidator()
{
RuleFor(x => x.FirstName)
.NotEmpty()
.WithMessage("First name is required.");
}

We’ve added a constructor and defined our first validation rule for the FirstName property – it must not be empty or a value is required (the Fluent Validation framework defines “empty” as null or an empty string). Run the test again. Our test passed! We are one step closer to coding Nirvana. Using a TDD approach, we would normally follow a “red-green-refactor” process of writing a failing test, then writing enough code to get the test to pass, then refactor the code to production quality. I am going to skip ahead slightly and just include all of the remaining tests for the FirstName property:

[TestMethod]
public void FirstName_EmptyString_ValidationFails()
{
//arrange
target.FirstName = string.Empty;

//act
ValidationResult result = validator.Validate(target, x => x.FirstName);

//assert
validator.ContainsRuleFor<Employee>(x => x.FirstName);
result.AssertValidationFails("First name is required.");
}

[TestMethod]
public void FirstName_Null_ValidationFails()
{
//arrange
target.FirstName = null;

//act
ValidationResult result = validator.Validate(target, x => x.FirstName);

//assert
validator.ContainsRuleFor<Employee>(x => x.FirstName);
result.AssertValidationFails("First name is required.");
}

[TestMethod]
public void FirstName_GreaterThan50Characters_ValidationFails()
{
//arrange
target.FirstName = "".PadRight(51, 'X');

//act
ValidationResult result = validator.Validate(target, x => x.FirstName);

//assert
validator.ContainsRuleFor<Employee>(x => x.FirstName);
result.AssertValidationFails("First name cannot exceed 50 characters.");
}

[TestMethod]
public void FirstName_StartsWithANumericValue_ValidationFails()
{
//arrange
target.FirstName = "24Name";

//act
ValidationResult result = validator.Validate(target, x => x.FirstName);

//assert
validator.ContainsRuleFor<Employee>(x => x.FirstName);
result.AssertValidationFails("First name contains invalid characters.");
}

[TestMethod]
public void FirstName_ContainsANumericValue_ValidationFails()
{
//arrange
target.FirstName = "Name24Number";

//act
ValidationResult result = validator.Validate(target, x => x.FirstName);

//assert
validator.ContainsRuleFor<Employee>(x => x.FirstName);
result.AssertValidationFails("First name contains invalid characters.");
}

[TestMethod]
public void FirstName_EndsWithANumericValue_ValidationFails()
{
//arrange
target.FirstName = "Name24";

//act
ValidationResult result = validator.Validate(target, x => x.FirstName);

//assert
validator.ContainsRuleFor<Employee>(x => x.FirstName);
result.AssertValidationFails("First name contains invalid characters.");
}

[TestMethod]
public void FirstName_IsANumericValue_ValidationFails()
{
//arrange
target.FirstName = "254";

//act
ValidationResult result = validator.Validate(target, x => x.FirstName);

//assert
validator.ContainsRuleFor<Employee>(x => x.FirstName);
result.AssertValidationFails("First name contains invalid characters.");
}

[TestMethod]
public void FirstName_NotEmptyLessThan50CharactersWithNoNumericValues_ValidationPasses()
{
//arrange
target.FirstName = "Jack";

//act
ValidationResult result = validator.Validate(target, x => x.FirstName);

//assert
validator.ContainsRuleFor<Employee>(x => x.FirstName);
result.AssertValidationPasses();
}

Some of these tests will fail because we have not updated our validator to include the rules for string length and RegEx validation. Update the EmployeeValidator class constructor so it looks like this:

public EmployeeValidator()
{
RuleFor(x => x.FirstName)
.NotEmpty()
.WithMessage("First name is required.")
.Length(0, 50)
.WithMessage("First name cannot exceed 50 characters.")
.Matches(@"^[A-Za-z\-\.\s]+$")
.WithMessage("First name contains invalid characters.");
}

Our validation rules have now been extended to include a range validation for our string (0 to 50 characters), and a RegEx validation for letters, a dash, a period, and a space. You may be wondering why I chose to set the minimum length to 0 when our requirements state that the FirstName property is required. Well, the NotEmpty rule covers that, and I only want to show the user what state requires their attention. If the Length validation was set to Length(1, 50) and the user had not provided a value for FirstName, the validation result would show two error messages: “First name is required”, “First name cannot exceed 50 characters”. The first message makes sense, but the second doesn’t apply since the user has yet to supply a value. After that lengthy explanation, run the tests again and they should all pass. Whew! We’ve covered a lot of ground this time. You've seen how to use FluentValidation with Test-Driven Development, and we’ve created a few extension methods to help us. We’ve done some pretty extensive testing of our business rules, and you can see how to do some really powerful string validation including limiting the number of characters and using RegEx. As an exercise for the reader, implement the tests for the LastName property. Next time we will look at validation for other data types and how to create custom validation rules.

1 comment:

  1. Thanks for posting this. It really helped me out. I had to add some code to cater for a UnaryExpression to get one of my own tests to pass. I have pasted this below (the formatting will probably be rubbish :-) ). Hope it helps.
    Cheers,
    Gordon

    public static void ContainsRuleFor(this IValidator validator, Expression> propertyExpression)
    {
    string propertyToTest;
    if ((propertyExpression.Body is UnaryExpression))
    propertyToTest = (((UnaryExpression) propertyExpression.Body).Operand as MemberExpression).Member.Name;
    else {
    propertyToTest = ((MemberExpression) propertyExpression.Body).Member.Name;
    }
    ContainsRuleFor(validator, propertyToTest);
    }

    ReplyDelete