Wednesday, February 24, 2010

On the Utility of TestPropertyAttribute

On the project I’ve been working on lately, I’ve been using the unit test stuff that ships with Visual Studio/.NET, in the Microsoft.VisualStudio.TestTools.UnitTesting namespace. For my purposes, I’ve found it to be adequate, and it has the benefit that I don’t need to include any additional libraries.

I recently came across one use of it, however, that took me some time to figure out. Primarily because the documentation on this feature sucks pretty bad. It’s the TestPropertyAttribute, and it’s useful enough to be worth knowing about. So I thought I would share.

If you read the documentation for TestPropertyAttribute, your first reaction might well be the same as mine was: “Huh? That looks like it adds precisely no value over any other custom attribute I might write.” Because the docs show that to retrieve the value, you reflect against the test method it’s defined on, which is pretty silly. However, in experimenting, I found that there’s an equally poorly documented property on the TestContext class called Properties that you can use to get the value of this attribute.

Still, if you only need a value within the TestMethod itself, there’s not much value in it, as it’s no better than just hardcoding the value within the body of the test. Where TestProperty shines, though, is that the properties for the current test are also available during the TestInitialize method. That means you can bung a whole bunch of common setup code into your TestInitialize method, and parameterize its behavior based on properties defined on the tests themselves. A little code might help demonstrate what I mean:


1:
using System;


   2:  using System.IO; 


   3:  using Microsoft.VisualStudio.TestTools.UnitTesting;


   4:   


   5:  namespace Demo


   6:  {


   7:      [TestClass]


   8:      public class ThingTests


   9:      {


  10:          // Note that we have to have a TestContext, and it must


  11:          // be named TestContext


  12:          public TestContext TestContext { get; set; }


  13:   


  14:          // A simple field that we're going to initialize to 


  15:          // some value in the Initialize method.  


  16:          private string _thing; 


  17:          


  18:          [TestInitialize]


  19:          public void Initialize()


  20:          {


  21:              // Obviously we could do something much more complex here.


  22:              if (TestContext.Properties.Contains("thing"))


  23:              {


  24:                  _thing = TestContext.Properties["thing"] as string;


  25:              }


  26:              else


  27:              {


  28:                  _thing = "default"; 


  29:              }


  30:          }


  31:   


  32:          [TestMethod]


  33:          public void TestAgainstDefaultSetup()


  34:          {


  35:              Assert.AreEqual("default", _thing); 


  36:          }


  37:   


  38:          [TestMethod]


  39:          [TestProperty("thing", "non-default")]


  40:          public void TestAgainstCustomSetup()


  41:          {


  42:              Assert.AreEqual("non-default", _thing); 


  43:          }


  44:      }


  45:  }



Basically, we’ve got a field _thing that we usually want to leave at the default value of “default”, but in some tests we want to override to some other value. Moreover, we want to override it in the Initialize method, since that’s where we’re doing our setup that’s common across tests. – maybe we have fairly complicated setup that needs to differ only slightly in some tests. So we use the TestProperty attribute to set a property named “thing” to whatever we like. Then, in the Initialize method, we can use TestContext.Properties to check for the presence of that value and take appropriate action.

I’ve made the code intentionally trivial to more easily demonstrate the concept, but I’m sure you can visualize more realistic scenarios where this might come in handy. For example, I’ve used it to customize which directory my Initialize method operates on.