Mark Gilbert's Blog

Science and technology, served light and fluffy.

Reducing the Tedium: Generalized Unit Tests via Reflection

In the course of developing a new class, especially one that is tied to Castle ActiveRecord, I will usually add one or more String properties.  Unless there is some reason to make these nullable, I usually modify the getters to return either an empty string or a valid value.  Knowing that the property will only be in one of these two states makes it easier to use that property in expressions like MyObject.MyProperty.Contains("blah").  I don’t have to worry about a null reference exception here if I know MyProperty can’t return a null.

To ensure that the properties are up to snuff, I will invariably write a series of five unit tests, per property:

1) Initializing the class – property should return empty string
2) Setting the property to null – property should return empty string
3) Setting the property to an empty string – property should return empty string
4) Setting the property to some whitespace – property should return empty string
5) Setting the property to a valid value – property should return that value.

As you can imagine, writing these five for each String property gets tedious.  A couple of weeks ago, I wondered if I could automate these tests – specifically, could I write something that would automatically and dynamically check every String property on a class to make sure each one passed these five conditions?

As it turns out, the answer is a resounding yes.  Here is my NUnit test for Strings:

 

    [TestFixture]
    public class PropertyTests
    {
        Type[] _TypesToCheck = { 
                                   typeof(PropertyTestsViaReflection.NS_A.ClassA), 
                                   typeof(PropertyTestsViaReflection.NS_B.ClassB),
                                   typeof(PropertyTestsViaReflection.NS_C.ClassC)
                               };

        [Test]
        public void StringProperties_DefaultToEmptyString()
        {
            String TestValue, ClassName;
            PropertyInfo[] ClassProperties;
            Object ClassInstance;

            for (int i = 0; i < this._TypesToCheck.Length; i++)
            {
                ClassName = this._TypesToCheck[i].Name;
                ClassProperties = this._TypesToCheck[i].GetProperties();
                ClassInstance = Activator.CreateInstance(this._TypesToCheck[i]);

                System.Diagnostics.Trace.Write(String.Format("Now testing {0}...", ClassName));

                foreach (var PropertyUnderTest in ClassProperties.Where(p => p.PropertyType == typeof(String)))
                {
                    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));
                }

                System.Diagnostics.Trace.WriteLine("completed");
            }
        }
    }

First, I define a hard-coded list of classes called "_TypesToCheck".  (I did this more for convenience than anything else; at the end of this post I suggest a better way.)  For each of these types, I grab the name of the class (to be used with the error messages), the list of properties to check, and instantiate an instance of the class.

I boil the list of properties down to just the ones that are of type String, and then iterate over each of those, running my five tests.  If any of these tests fail for any of the properties for any of the classes, the test reports the failure and stops.

I pieced this method together from several sources:


In addition to checking the String properties, I also perform a couple of tests on any properties with the name "ID".  This is an ActiveRecord standard, and I want to make sure that the ID properties are 0 initially, and that it can’t be assigned a negative value (this would be just another test in the same fixture).  The basic structure is the same, but instead of looking for properties of type “String”, I boil my list down to properties with the name “ID”":

        [Test]
        public void IDProperties_DefaultTo0()
        {
            long TestValue;
            String ClassName;
            PropertyInfo[] ClassProperties;
            Object ClassInstance;

            for (int i = 0; i < this._TypesToCheck.Length; i++)
            {
                ClassName = this._TypesToCheck[i].Name;
                ClassProperties = this._TypesToCheck[i].GetProperties();
                ClassInstance = Activator.CreateInstance(this._TypesToCheck[i]);

                System.Diagnostics.Trace.Write(String.Format("Now testing {0}...", ClassName));

                foreach (var PropertyUnderTest in ClassProperties.Where(p => p.Name.Equals("ID", StringComparison.CurrentCultureIgnoreCase)))
                {
                    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));
                }

                System.Diagnostics.Trace.WriteLine("completed");
            }

        }

 

These tests certainly don’t take care of all testing for a class, but it certainly does handle most of the basics.  When I add a new class, I simply update the _TypesToCheck array to reference it.  If I add a new property to one of the covered classes, the test fixture picks up on that immediately and tells me when the property isn’t behaving properly.

This is only a first step.  I can easily see a couple of enhancements that might prove useful:

  • Instead of looking at the property name or type, attach a validator (custom or otherwise) attribute that will identify what kinds of tests to perform on that property.  If it is a String property, run the String tests; if it is a Date property, make sure the date is not DateTime.MinValue, etc..
  • Be able to decorate the classes, and then have the test fixture reflect over the entire assembly to find the classes to test.  This would replace the need for a hard-coded array of assemblies.

Enjoy!

About these ads

July 3, 2013 - Posted by | Visual Studio/.NET

Sorry, the comment form is closed at this time.

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: