Sunday, August 21, 2011

Emitting Unit Tests from Assemblies Part 2


In my previous post I've illustrated how to setup the necessary utility functions for emitting C# code from assemblies. In this post I'll tie it all together and finish up the application so that we can generate equality unit tests from an assembly.

The first step is of course to define a program entry point.

static void Main(string[] args)
{

In my application this will load the assembly and generate unit tests for each of the types which inherit from ANY (the purpose of this application is to generate equality tests for Everest, you can modify this to suit your needs). I'm using a lambda expression to perform this operation:

var types = typeof(II).Assembly.GetTypes();
var anyTypes = Array.FindAll(types, o => !o.IsAbstract &&
    o.GetCustomAttributes(typeof(ObsoleteAttribute), true).Length == 0 &&
    typeof(ANY).IsAssignableFrom(o) &&

    o.GetMethod("Equals", new Type[] { o }) != null);

The lambda expression that searches each type looks confusing but can be easily explained. The variable types is an array of all types in the assembly which defines the II type (ie: the assembly we want to search), the lambda clauses find all types in that assembly that:


!o.IsAbstractWhere the type is not abstract (ie: we can instantiate an instance of the type)
o.GetCustomAttributes(typeof(ObsoleteAttribute), true).Length == 0The type is not obsolete (ie: there are no ObsoleteAttribute defined on the type)
typeof(ANY).IsAssignableFrom(o)The type can be assigned to a variable of type ANY (ie: is a ANY)
o.GetMethod("Equals", new
Type[] { o }) != null
The type has a method called Equals that accepts a parameter of itself (ie: equality can be determined between two instances of the type)


Next, we'll iterate through each of the types that we've found and will generate a unit test file (if I were a Java developer I'd call this an Iterator pattern to make it seem more impressive, but I like to keep things simple)

foreach (Type t in anyTypes)
     GenerateTestFile(t);

Before I go into the contents of the GenerateTestFile method, I'll explain a little bit about the structure of a Visual Studio unit test. All unit tests are represented as methods within a test class. The test class defines the test context as well as any setup and teardown code. Since I didn't want to store this as a string (very un-maintainable), I created a template file and added it as a resource called Templates.

GenerateTestFile does exactly what you'd expect, it generates the test file. First, it checks if T is a generic type definition and if it is, creates a generic type using the INT class (in .NET each generic class is a new type as it uses reified generics).

if (t.IsGenericTypeDefinition)
{
   // Create a generic type of INT
   List<Type> genericArguments = new List<Type>();
   foreach (Type genParm in t.GetGenericArguments())
       genericArguments.Add(typeof(INT));
   t = t.MakeGenericType(genericArguments.ToArray());
}

This little snippet of code will create a list (or array) of generic arguments of INT for each generic argument available in the type definition. For example, if it encounters QTY then QTY<INT> is created, for RTO the type RTO<INT,INT> is created.

Next we do some pretty standard .NET stuff. I always use this pattern for generating files. I'm not going to outline the code here are it is pretty easy to follow in the attached file

TextWriter tw = null;
try
{
   // Code Here

}
finally
{
   if(tw != null)

      tw.Close();
}

The complete source for this program is available here: http://pastebin.com/P06dRAnu

Remember, a piece of software is only as good as the testing and QA processes it passes through. Emitting unit tests from a target test assembly is a great start for mundane tests like Equality or Comparisons but you will eventually have to manually write unit tests for more complex logic in your application. As an example, Everest's test battery consists of 9,500 unit tests of which approximately 1,000 are hand written and the rest are generated.

Happy testing!

No comments:

Post a Comment