Mark Gilbert's Blog

Science and technology, served light and fluffy.

Reducing the Tedium: Redux

Back in July, I wrote “Reducing the Tedium: Generalized Unit Tests via Reflection” which introduced a generalized unit test for strings and IDs in a pre-defined list of classes.  This was my first stab at trying to automate some of the more tedious tests I typically write for properties of this sort.  With my latest project, I was able to extend this even further with a new “PropertyValidator” class.

My first task was to eliminate the need for the explicit “TypesToCheck” array that defined which classes to test.  Since a lot of my use cases was checking business classes in the same assembly, I started by building a method that could take the name of the assembly to test as a parameter, open that assembly up via Reflection, and find all of the classes in it.

I started by repackaging my original code into two main functions – one for validating strings:

private static void ValidateStrings(Type CurrentType)
{
    String TestValue, ClassName;
    PropertyInfo[] ClassProperties;
    Object ClassInstance;
    
    ClassName = CurrentType.Name;
    ClassProperties = CurrentType.GetProperties().Where(p => p.PropertyType == typeof(String) && p.GetSetMethod() != null).ToArray();
    ClassInstance = Activator.CreateInstance(CurrentType);

    foreach (var PropertyUnderTest in ClassProperties)
    {
        if (ShouldSkipProperty(PropertyUnderTest)) { continue; }

        TestValue = (String)PropertyUnderTest.GetValue(ClassInstance, null);
        Assert.IsEmpty(TestValue, String.Format("{0}.{1} did not initialize properly", ClassName, PropertyUnderTest.Name));

        PropertyUnderTest.SetValue(ClassInstance, null, null);
        TestValue = (String)PropertyUnderTest.GetValue(ClassInstance, null);
        Assert.IsEmpty(TestValue, String.Format("{0}.{1} did not handle null properly", ClassName, PropertyUnderTest.Name));

        PropertyUnderTest.SetValue(ClassInstance, "", null);
        TestValue = (String)PropertyUnderTest.GetValue(ClassInstance, null);
        Assert.IsEmpty(TestValue, String.Format("{0}.{1} did not handle an empty string properly", ClassName, PropertyUnderTest.Name));

        PropertyUnderTest.SetValue(ClassInstance, "  ", null);
        TestValue = (String)PropertyUnderTest.GetValue(ClassInstance, null);
        Assert.IsEmpty(TestValue, String.Format("{0}.{1} did not handle a blank string properly", ClassName, PropertyUnderTest.Name));

        PropertyUnderTest.SetValue(ClassInstance, "abc123", null);
        TestValue = (String)PropertyUnderTest.GetValue(ClassInstance, null);
        Assert.AreEqual("abc123", TestValue, String.Format("{0}.{1} did not handle a valid string properly", ClassName, PropertyUnderTest.Name));
    }
}

 

And the other for validating IDs:

private static void ValidateIDs(Type CurrentType)
{
    long TestValue;
    String ClassName;
    PropertyInfo[] ClassProperties;
    Object ClassInstance;

    ClassName = CurrentType.Name;
    ClassProperties = CurrentType.GetProperties();
    ClassInstance = Activator.CreateInstance(CurrentType);

    foreach (var PropertyUnderTest in ClassProperties.Where(p => IsIDToValidate(p)))
    {
        if (PropertyUnderTest.GetCustomAttributes(typeof(ObsoleteAttribute), true).Count() > 0) { continue; }

        TestValue = (long)PropertyUnderTest.GetValue(ClassInstance, null);
        Assert.AreEqual(0, TestValue, String.Format("{0}.{1} did not initialize properly", ClassName, PropertyUnderTest.Name));

        PropertyUnderTest.SetValue(ClassInstance, 0, null);
        TestValue = (long)PropertyUnderTest.GetValue(ClassInstance, null);
        Assert.AreEqual(0, TestValue, String.Format("{0}.{1} did not handle being set to 0 properly", ClassName, PropertyUnderTest.Name));

        PropertyUnderTest.SetValue(ClassInstance, -1, null);
        TestValue = (long)PropertyUnderTest.GetValue(ClassInstance, null);
        Assert.AreEqual(0, TestValue, String.Format("{0}.{1} did not handle being set to a negative properly", ClassName, PropertyUnderTest.Name));
    }
}

 

These do the same basic checks on each string or ID property as described in the July post.  They make use of three helper functions – ShouldSkipType, ShouldSkipProperty, and IsIDToValidate.  I’ll describe these in turn:

private static bool ShouldSkipType(Type CurrentType)
{
    return CurrentType.GetCustomAttributes(typeof(PropertyValidator.Attributes.DoNotPerformBasicValidation), true).Length > 0 
        || CurrentType.IsGenericType 
        || CurrentType.IsAbstract
        || CurrentType.FullName.StartsWith("PostSharp");
}

The ShouldSkipType function returns True if the class:

  • is a generic type
  • is an abstract type
  • starts with “PostSharp” (this is a logging library that I started using this summer, and its classes are added to mine post-build; trying to run my test code against these classes cause problems, so I just skip them entirely)
  • has the custom “DoNotPerformBasicValidation” attribute attached.

If any of these conditions is met, the class is skipped.  I found a need to create exceptions to the rule of “check every class in this assembly”, so I created the “DoNotPerformBasicValidation” attribute that I could decorate a specific class in an assembly to be skipped.  This attribute has no logic of its own – it is merely used as a flag on the class:

using System;
using System.Collections.Generic;
using System.Linq;

namespace PropertyValidator.Attributes
{
    [AttributeUsage(AttributeTargets.All)]
    public class DoNotPerformBasicValidation : Attribute 
    { 
    }
}

This attribute can be applied to not only classes, but also individual properties, and the ShouldSkipProperty function looks for that:

private static bool ShouldSkipProperty(PropertyInfo CurrentProperty)
{
    return CurrentProperty.GetCustomAttributes(typeof(PropertyValidator.Attributes.DoNotPerformBasicValidation), true).Length > 0;
}

This function returns True if the custom attribute is found on the current property being evaluated.

Finally, to determine if a given property is a string, “ValidateStrings” merely looks at the property’s type.  To determine if it is an ID to be checked is a little trickier.  I can’t rely solely on its base type, so instead I require that property to be treated as an ID is marked as such, with another custom attribute:

using System;
using System.Collections.Generic;
using System.Linq;

namespace PropertyValidator.Attributes
{
    [AttributeUsage(AttributeTargets.Property)]
    public class ValidateAsID : Attribute 
    {
    }
}

When the “ValidateAsID” attribute is applied to a property, the “IsIDToValidate” function returns true:

private static bool IsIDToValidate(PropertyInfo CurrentProperty)
{
    return CurrentProperty.GetCustomAttributes(typeof(PropertyValidator.Attributes.ValidateAsID), true).Length > 0;
}

 

The “ValidateStrings” and “ValidateIDs” functions need a list of classes to open and examine.  That list is provided by the PropertyValidator’s “Validate” methods:

public static void Validate(String AssemblyPartialName)
{
    Validate(AssemblyPartialName, null);
}
public static void Validate(String AssemblyPartialName, String ClassName)
{
    Assembly AssemblyToValidate;
    Type[] AssemblyTypes;

    AssemblyToValidate = Assembly.Load(AssemblyPartialName);
    if (AssemblyToValidate == null) { throw new Exception(String.Format("Could not load {0}", AssemblyPartialName)); }

    AssemblyTypes = AssemblyToValidate.GetTypes();

    foreach (Type CurrentType in AssemblyTypes)
    {
        try
        {
            if (ShouldSkipType(CurrentType)) { continue; }
            if (ClassName != null && CurrentType.Name != ClassName) { continue; }

            System.Diagnostics.Trace.WriteLine(String.Format("Now testing '{0}'", CurrentType.Name));

            ValidateStrings(CurrentType);
            ValidateIDs(CurrentType);
        }
        catch (Exception ex)
        {
            throw new Exception(String.Format("Error validating the type '{0}'", CurrentType.Name), ex);
        }
    }
}

The first takes only the assembly name, and will attempt to check every class in that assembly.   The second will allow you to check a single specific class in an assembly.  For each class being checked, then, the method will invoke both “ValidateStrings” and “ValidateIDs”.

To use it, then, I can do something as simple as

PropertyValidator.Validate(“MyAssembly”);

or

PropertyValidator.Validate(“MyAssembly”, “MyClass”);

***

PropertyValidator gives me a much better testing framework for properties than before, and is very easy to use.  I still have a dependency on NUnit, however, and it doesn’t do anything with other data types such as datetimes.  Perhaps the next iteration.

Advertisements

December 23, 2013 - Posted by | Visual Studio/.NET

Sorry, the comment form is closed at this time.

%d bloggers like this: