Unit Testing your C# public interface

These days, it’s common practice to unit-test our code. Whether you’re a TDD, or simply doing white box / black box unit-testing post coding, the final act of having a unit test in place to test conformancy to certain requirements is good practice.

But what requirements should we be testing?

The common approach is to test the functionality of a unit of code, i.e. for a particular set of inputs, test for the expected output(s). This is good.

But with OO based-design, and win-forms data-binding, I believe we need to go one step further. We need to test the public interface declaration – the classes contract to the outside world. We need to test that the public interface for both the getters and setters of a class do not change scope or name.

Why do I say name? Win-Forms data-binding is done via a String parameter which specifies the member name to be bound.

If a member name is changed (either manually or F2), more often than not, the data-binding that is bound to that member will not be updated, the program will compile successfully, and the problem will not be found until run-time (hopefully found before the product is shipped).

So how can we test that our public interface has not changed? By using Reflection, we can write a unit-test helper class that will allow us to test the public interface declaration. The goals I’ve set for this PropertyTester is that it should:

  • Fail if a property’s getter or setter change’s scope
  • Fail if a property’s name changes
  • Fail if a property is not tested

Property Tester Helper Class

public class PropertyTester<T> : IDisposable
    where T : class
{
    /// <summary>
    /// Creates an instance of the <c>PropertyTester</c> class.
    /// This class allows you to check that the setters and getters for properties on the
    /// target are as expected. When used in a using statement, the Dispose call will
    /// confirm that all properties were tested.
    /// </summary>
    /// <param name="target">The target object of interest</param>
    public PropertyTester()
    {
        // Store away the target
        this.target = typeof(T);

        // Create a list of all the properties expected to be tested.
        const BindingFlags flags = BindingFlags.Public | BindingFlags.Instance;
        this.targetPropertyList = new List<PropertyInfo>(target.GetProperties(flags));
    }

    /// <summary>
    /// Checks that the property getters and setters accessors for an object are as expected.
    /// </summary>
    /// <param name="memberName">The property of the object to be tested.</param>
    /// <param name="canRead">Whether we expect that we can read this property.</param>
    /// <param name="canWrite">Whether we expect that we can write to this property.</param>
    public void CheckGetterSetter(string propertyName, bool canRead, bool canWrite)
    {
        // Confirm that the getters and setters for the property are correct.
        // Get a handle to the Property of the interested target.
        PropertyInfo property = this.targetPropertyList.FirstOrDefault(p => p.Name == propertyName);
        if (property == null)
        {
            // If the test code fails here, then a Property name has changed on the target.
            // It is important that ALL GUI code that refers to this Property name is updated.
            // NOTE: The compiler may not find all of the GUI references for this Property name
            // as the ObjectListView class uses reflection for setting/getting values.
            this.failure = true;
            String failed_message = String.Format("Property \"{0}\" does not exist for object \"{1}\"", propertyName, target.ToString());
            Assert.Fail(failed_message);
        }

        // Confirm the existance of the public getter.
        if (canRead != (property.CanRead && property.GetGetMethod() != null))
        {
            this.failure = true;
            string failed_message = String.Format("Property \"{0}\": expected getter is {1}, actual getter is {2}.", propertyName, canRead, !canRead);
            Assert.Fail(failed_message);
        }

        // Confirm the existance of the public setter.
        if (canWrite != (property.CanWrite && property.GetSetMethod() != null))
        {
            this.failure = true;
            string failed_message = String.Format("Property \"{0}\": expected setter is {1}, actual setter is {2}.", propertyName, canWrite, !canWrite);
            Assert.Fail(failed_message);
        }

        // Remove properties with this name from the targetPropertyList list.
        // Note: We're removing all instances of properties with the name defined
        //   in the 'memberName' parameter as there could be more than one, as
        //   is the case when properties can be redefined in a sub-class with
        //   the 'new' keyword.
        this.targetPropertyList.RemoveAll(p => p.Name == propertyName);
    }

    /// <summary>
    /// Confirms that all property checker/getters were tested on the target.
    /// </summary>
    public void Dispose()
    {
        // If a failure hasn't occured during the test and the target property list
        // still contains objects, then this means some properties were not correctly tested.
        if ((this.failure == false) && (targetPropertyList.Count > 0))
        {
            string failed_message = "";
            foreach (PropertyInfo pinfo in this.targetPropertyList)
            {
                failed_message += String.Format("Property \"{0}\" was not tested for object \"{1}\"\r\n", pinfo.Name, this.target.ToString());
            }
            Assert.Fail(failed_message);
        }
    }

    /// <summary>
    /// The target object to be tested.
    /// </summary>
    private Type target = null;

    /// <summary>
    /// The list of properties for the target expected to be tested.
    /// </summary>
    private List<PropertyInfo> targetPropertyList = null;

    /// <summary>
    /// Whether a failure has been detected or not.
    /// </summary>
    private bool failure = false;
}

Example

To use this PropertyTester, let’s build a simple Person class

/// <summary>
/// Example Person class to demonstrate how to use the PropertyTester
/// <summary>
public class Person
{
    public string Name { get; private set; }
    public int Age { get; set; }
}

Then in your unit-testing code, all we need to do is

/// <summary>
/// Tests the properties getters and setters access to the Person Class.
/// </summary>
[TestMethod]
public void TestPropertyGetterSetter()
{
    // Create a PropertyTester to facilitate the testing of the getters/setters.
    using (PropertyTester<Person> propTester = new PropertyTester<Person>())
    {
        // Check the expected access to each property.
        propTester.CheckGetterSetter("Name", true, true);
        propTester.CheckGetterSetter("Age", true, false);
    }
}

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>