Tuesday, May 26, 2009

An easier way to track parameters in unit testing for Visual Studio (mstext.exe)

One downside in unit testing is having an easy way view and use the test parameters. Sure you can put them in the code, but its hard to see how it varies from method to method. Ok.. maybe 'hard' isn't it, but 'not as clean as having all test parameters in one spot' I'll say. So I wrote a quick routine to extract the parameters. note:

I created a method called SetPropsToAttributes() and also GetAttributeValue().

For SetPropsToAttributes, Pass in an object and based on the TestProperty() attributes, it will find the matching property (not case sensitive) and set its value. So you can pass in an object and any attributes that are named the same as the object's properties, will have the values set to whatever are in the attributes. So all test information is kept then at the top of the method.

For GetAttributeValue, the idea here is an easier way to get a single value:
If I have this method:


[TestMethod]
[TestProperty("UserName","Adam Tuliper")]
[TestProperty("Password","nobodyknows")]
public void TestLogin()
{
LoginHandler handler = new LoginHandler();
handler.Login((string)("UserName"), (string)GetAttributeValue("Password"));
}


You can see here I easily get the parameters. I don't have to look through code to get test information. Every method has it at the top. This also makes it easy to copy and paste information into a test script for documentation purposes.



On the helper methods below I use [MethodImpl(MethodImplOptions.NoInlining)] because this is meant for debug code only in your _test program_ (not in the actual bytes being unit tested) which you would have during a unit test. The stack frame tracking and debug information is different (debug information is missing unless you do a release build with the pdbonly advanced option selected - this should work in that scenario but I haven't tried it) depending on options selected) in a release build. When you compile a release build, there are optimizations performed and the compiler may decide to inline the method calls, thus breaking the way this routine finds the method that called it. Read again: this will break in release builds - it is for debug code only, but then again thats the whole purpose of this posting : )

One more note - Do not call SetPropsToAttributes and GetAttribute from more than one level up.. Ie dont do this: Method1() - > Method2() -> GetAttribute(for memthod1)
as the code only looks up one stack frame on top of GetAttribute to determine the calling method.


So we
1. Create method based
2. Declare TestProperty attributes with our test data. null, int, strings, etc
3. Pass in the object we are going to unit test and values get assigned in the method SetPropsToAttribValues
4. Run test
5. Assert return codes



/// <summary>
/// A unit test method providing a sample way to automatically assign
/// test attributes to Properties in an object
///</summary>
[TestMethod()]
[TestProperty("TestProperty1", "Something")]
[TestProperty("TestProperty2", "SomethingElse")]
public void SomeUnitTestMethod
{
SomeCustomObject testObject = new SomeCustomObject();
//This call will automatically set testObject.TestProperty1 to "Something" for ex.
SetPropsToAttribValues (testObject);
int retVal = testObject.SomeMethod();

Assert.IsTrue(retVal==0, "Return value was unexpected:" + retVal.ToString());

}


/// <summary>
/// Uses reflection to look at the calling method's TestProperty attributes
/// and will set values on properties matching those attribute names with the attribute values.
/// Property names are not case sensitive for this implementation to help avoid unit test typos.
/// </summary>
/// <param name="obj"></param>
[MethodImpl(MethodImplOptions.NoInlining)]
private void SetPropsToAttribValues(object obj)
{
//Get name of attribute. if it exists and value != null set it.
Type t = GetType();
Type attributeType = typeof(TestPropertyAttribute);
//hop up the stack frame to the caller so we can get its attributes
object[] attributes = new System.Diagnostics.StackFrame(1, false).GetMethod().GetCustomAttributes(attributeType, false);

for (int i = 0; i <= attributes.GetUpperBound(0); i++)
{
string propertyName = ((TestPropertyAttribute)attributes[i]).Name.ToLower();

PropertyInfo[] properties = obj.GetType().GetProperties();
foreach (PropertyInfo pi in properties)
{
//Made to be not case sensitive to help avoid test script typos.
if (pi.Name.ToLower() == propertyName)
{
pi.SetValue(obj, ((TestPropertyAttribute)attributes[i]).Value, null);
}
}
}
}

/// <summary>
/// Looks up attribute values for a particular method. They are used here for sending in values for the test cases.
/// This reads all [TestProperty] attribute values
/// </summary>
/// <param name="attributeName"></param>
/// <returns>attribute value as specified in [TestProperty] attribute, throws an exception if not found.</returns>
[MethodImpl(MethodImplOptions.NoInlining)]
private object GetAttributeValue(string attributeName)
{

Type t = GetType();
Type attributeType = typeof(TestPropertyAttribute);
//hop up the stack frame to the caller so we can get its attributes
object[] attributes = new System.Diagnostics.StackFrame(1, false).GetMethod().GetCustomAttributes(attributeType, false);

for (int i = 0; i <= attributes.GetUpperBound(0); i++)
{
if (((TestPropertyAttribute)attributes[i]).Name.ToLower() == attributeName.ToLower())
{
return ((TestPropertyAttribute)attributes[i]).Value;
}
}
throw new Exception("Could not find attribute " + attributeName);
}

3 comments: