Wednesday, 20 July 2011

Unit Testing C# Custom Attributes with NUnit

I've been experimenting with TDD and as usual I've seemed to pick a non-standard problem to start with.  In this case I was creating a new C# Custom Attribute class, e.g.

public class FunkyAttribute : Attribute
{
 public int SettingOne { get; set; }
 public string Name { get; set; }
}

which would be used such as

[Funky(Name="SomeThingFunkierThanJust_f")]
public static int f() { return 7; }

Testing this is a little strange as rather than having a standard test method which invokes a method and asserts the result, e.g.


[Test]
public void TestThat_f_Returns7()
{
 Assert.AreEqual(7, SomeClass.f());
}

There is no method to call because an Attribute is applied to an element of the program such as the definition of f() above.  In the end I settled on the following

public class FunkyTester
{
 [Test]
 [Funky(Name="RipSnorter")]
 public void TestThatNameIsRipSnorter()
 {
  TestAttrProperty<FunkyAttribute, string>(new StackFrame().GetMethod(), "Name", "RipSnorter");
 }

 [Test]
 [Funky(SettingOne=77)]
 public  void TestThatSettingOneIs77()
 {
  TestAttrProperty<FunkyAttribute, int>(new StackFrame().GetMethod(), "SettingOne", 77);
 }

 // Helpers
 private void TestAttrProperty<TAttr, TProp>(MethodBase method, string argName, TProp expectedValue)
 {
  object[] customAttributes = method.GetCustomAttributes(typeof(TAttr), false);

  Assert.AreEqual(1, customAttributes.Count());

  TAttr attr = (TAttr)customAttributes[0];

  PropertyInfo propertyInfo = attr.GetType().GetProperty(argName);

  Assert.IsNotNull(propertyInfo);
  Assert.AreEqual(typeof (TProp), propertyInfo.PropertyType);
  Assert.IsTrue(propertyInfo.CanRead);
  Assert.IsTrue(propertyInfo.CanWrite);
  Assert.AreEqual(expectedValue, (TProp)propertyInfo.GetValue(attr, null));
 }
}

This test class contains 2 test methods which are identified by the NUnit [Test] attribute.  In addition there is the use of the FunkyAttribute to be tested, i.e. [Funky].  The Custom Attribute is fairly simple with a default (parameterless) constructor and just two; differently typed properties both with a getter and a setter.  For each of these I wanted to check :

  • A Custom Attribute of the correct type existed on the test method.
  • That the property to be tested of the Custom Attribute had the expected name.
  • That the property to be tested of the Custom Attribute had the correct type.
  • That the value of property to be tested of the Custom Attribute could be set.
  • That the value of property to be tested of the Custom Attribute could be obtained.
  • That the property to be tested of the Custom Attribute had the expected value.

As I wanted to perform these tests for each property of the Custom Attribute a helper method was called for which in this case is TestAttrProperty.  This method is generic so can be used for any property of a Custom Attribute that has a public getter and setter.  It just takes a reference to an instance of the a method (which will always be a Test method) that the Custom Attribute is set on along with parameters for the property's name and its expected value.  The latter is a generic typed parameter (TProp) along with the type of the actual Custom Attribute (TAttr).

TestAttrProperty() then obtains all the Custom Attributes instances matching the type of TAttr.  There can only be zero or one; the success case being one!  By limiting the search to just the Custom Attribute type being tested this means that the NUnit TestAttribute instance is not included which means no filtering of the results is required prior the tests. Following that reflection is used to obtain the PropertyInfo of the property under test.  This is then used for execution the remaining of the tests.

The MethodInfo for the test method is easily obtained  from the current stack frame from the System.Diagnostics namespace

Hopefully, this little snippet shows a simple generic way to test Custom Attributes with NUnit.

A next step would be to add the ability to test that the Custom Attribute can be successful applied to data members.

Wait, there's more!  Following this parts two, three and four were written.

No comments: