Monday, 19 January 2009

Refactored: Fluent Test Data Builders Using Generic Extension Methods and Lambda Expressions

Working further with the Fluent Test Data Builder I was working on yesterday has led me to refactor what I had, and the result is, I think, even nicer!

I've added a new method SetValue, and changed the from getting the Member.Name to getting the PropertyInfo from the Member. The other significant change is that I'm now wrapping the SetLength and SetValue methods in try catch blocks so that I can intercept a TargetInvocationException and throw the InnerException in its place, this helps to ensure that when I use the ExpectedExceptionAttribute I am catching the exception I really expect. The drawback with this is the stack trace, but I'm content to live with that for now.

publicstatic T SetLength<T>(this T obj, Expression<Func<T, string>> expression, int length)
{
try
    {
        PropertyInfo member = getMemberExpressionMember(expression.Body);
        string currentValue = expression.Compile().Invoke(obj) ?? string.Empty;
        string newValue = currentValue.PadRight(length);

        member.SetValue(obj, newValue, null);

        return obj;
    }

    catch (TargetInvocationException tie)
    {
        if (tie.InnerException != null) throw tie.InnerException;
        throw;
    }
}
 
public static TObj SetValue<TObj, TVal>(this TObj obj, Expression<Func<TObj, TVal>> expression, TVal value)
{

    try
    {
        PropertyInfo member = getMemberExpressionMember(expression.Body);
        member.SetValue(obj, value, null);
        return obj;
    }
    catch (TargetInvocationException tie)
    {
        if (tie.InnerException != null) throw tie.InnerException;
        throw;
    }
}



So how am I making use of this fluent interface? Well the methods above are two of those I have written, and probably the most interesting, but I have others too. These fall into two main categories:



  1. setting default valid values;

  2. creating default valid objects.


For setting default valid values, these are done as ExtensionMethods on the type I'm interested in. For example:



        public static Extract WithDefaultTitle(this Extract extract)
        {
            extract.QualificationTitle = "Mr";
            extract;
        }


For creating default valid objects, this is also done with an ExtensionMethod on the type that I'm interested in. As my need for different default starting points increases then I can easily write more. Here's an example:


        public static Extract AsValid(this Extract extract)
        {
            return extract
                .WithDefaultCode()
                .WithDefaultTitle()
                .SetValue(e => e.Id, 111)
        }


With these methods available to me I can combine them with my SetValue and SetLength methods to then alter the state of any given property so that I can test the implications of the value set in a degree of isolation (at least consistency).  In particular I am using this to test that behaviour to validate my object works correctly given different combinations of property values on the object under test. Here's an example of this:


        [Test]
        [ExpectedException(typeof(InvalidOperationException), "An Id must be present.")]
        public void NullIdShouldThrowAnInvalidOperationExceptionOnValidate()
        {
            Extract extract = new Extract()
                .AsValid()
                .SetValue(e => e.Id, null);
            extract.Validate();
        }
 
        [Test]
        public void A_SurnameWithLessThan35CharactersShouldBePaddedTo35CharactersInFormattedProperties()
        {
            Extract extract = new Extract()
                .AsValid()
                .SetLength(e => e.Surname, 34);
                
            Assert.AreEqual(34, extract.Surname.Length);
            Assert.AreEqual(35, extract.FormattedProperties["Surname"].Length);
        }

Personally I'm happy with how this is progressing, but we'll see how it goes as I begin to push the current envelope a bit more.

1 comment:

Matt Freeman said...

Kewl, some interesting ideas, great to see fluent interfaces taking a place in the codebase. Your code snippet editor seems to have distorted the output a little though :D