Writing a Macro Recorder: Part 1 – C#
The source code can be downloaded here (Visual Studio 2008). Download and use at your own risk.
NUnit 2.5.0.9122 can be downloaded here.
I showed you in the last post how to generate C# code by listening to the IBindingList events. That was a small example, but as the number of commands grow and the parameters proliferate, the complexity of the generated code rises too. How to test this generated code?
The answer is obvious: compile the code that was generated. Although a bit of a hassle to set up, once in place, this isn’t just useful for testing generated macro code. It’s useful for testing *ANY* generated code. In the sample code, I will be using NUnit v2.5.0 to drive the tests. If you are using an earlier version, you will probably be OK if you just remove the nunit.framework reference and add your version in.
.Net is cool. The infrastructure comes with its own compiler! Well, technically, it comes with a C#, VB.NET and Managed C++ compiler and there are additional compilers for Cobol.NET, IronPython and pretty much every other language you can think of.
But first a few observations. In the last post, I was driving the binding list via the UI. Automated UI testing is a whole different field so I’m going to pretend I didn’t use a UI last time. Instead, it would be easier to drive the list from code so we know exactly what we expect to be generating. In the TestHarness/TestingBase.DoATest method you will find this:
// CSharp.cs // System.ComponentModel.BindingList people = new System.ComponentModel.BindingList (); // We've set up the handler that will forward changes to the macro recorder... // by driving the list directly, we can always view Recorder.Instance.Text // to see what has been generated so far. people.ListChanged += people_ListChanged; Person woo = new Person(); woo.Name = "WOO"; woo.Age = 33; people.Add(woo); Person hoo = new Person(); hoo.Name = "HOO"; hoo.Age = 50; people.Add(hoo); people.Remove(woo);
Hardly rocket science: I set up a listener to forward any collection changes to the macro recorder; then I set up the collection. If you step through the code, by the time you get to the end of what has been shown here, you can interrogate the Recorder.Instance.Text field and get hold of this code that was generated by your previous statements:

Before I go on: just think about how awesome that is! That text was generated behind the scenes and (in this case) is practically equivalent to the code that was used to create it. When driving the IBindingList, perhaps in a real application, the developer would have no idea that macros were being recorded behind the scenes as a result of what they did to that data structure. Take this one step further: if you have the context to record a macro, you have the context to record who did what, to what, where and when… in other words, a full Audit Trail. Priceless for troubleshooting and debugging or even to see how your application is really used.
But I digress. Back to the code. Clearly, we can’t compile that. It isn’t even structured correctly. There is no class around it, no using statements and… how will we test it? The best way I’ve found is to create a place holder text file like this:
// Templates/CSharp.TXT
//
using System;
namespace AutoGeneratedCodeTest
{
///
/// Static wrapper class.
///
public static class Runner
{
///
/// Test method to be invoked. Generated code will be substituted herein.
///
public static System.ComponentModel.IBindingList Run()
{
//%CONTENTS%//
return theBindingList1;
}
}
}
You need to substitute the place holder text with the generated code and attempt to compile it – that is step one. Congratulations: your code compiles. But you need to ensure that the operations you have just done, and generated code for, will reconstruct the object into the same state. In this case, it does not matter if the code is equivalent: what matters that the output is equivalent. For example: in the test harness I called .Remove(objectReference) but in the generated code it will generate .RemoveAt(theIndex).
The compilation step is easy. That can be found in the Language/CSharp.cs file but the key line is clearly this:
// CSharp.cs
//
CompilerResults results = provider.CompileAssemblyFromSource(parms, txt);
Look at the CSharp.txt template again. There is a static method called Run() that returns an IBindingList – the IBindingList that was constructed entirely by the generated macro code that we have substituted. After the substitution, our code looks complete and ready to run like this:
using System;
namespace AutoGeneratedCodeTest
{
///
/// Static wrapper class.
///
public static class Runner
{
///
/// Test method to be invoked. Generated code will be substituted herein.
///
public static System.ComponentModel.IBindingList Run()
{
System.ComponentModel.BindingList theBindingList1 = new System.ComponentModel.BindingList();
MacroSample.Person thePerson1 = new MacroSample.Person();
thePerson1.Name = @"WOO";
thePerson1.Age = 33;
theBindingList1.Add(thePerson1);
MacroSample.Person thePerson2 = new MacroSample.Person();
thePerson2.Name = @"HOO";
thePerson2.Age = 50;
theBindingList1.Add(thePerson2);
theBindingList1.RemoveAt(0);
return theBindingList1;
}
}
So with that infrastructure in mind, we can run the end of our test. The Compile method (in the case of CSharp) generates an Assembly… we can then ‘jump in’ to the generated assembly and obtain the list that was built up using our code:
// TestingBase.cs::DoATest // Assembly assembly = Compile(Recorder.Instance.Text) as Assembly; IBindingList result = Execute(assembly) as IBindingList; System.ComponentModel.BindingList macroPeople = result as System.ComponentModel.BindingList ; Assert.AreEqual(true, ListComparer.AreEqual(people, macroPeople));
I wrote a ListComparer class for this test. Recall that ‘people’ is the one we built up in our test. ‘macroPeople’ is what was built up in our generated macro code; the code we compiled and then jumped in to.
To sum up: that is one way to test generated C# code. There’s a million and one ways to do this: with the CodeDOM you could build up the target file programmatically and add the generated code under the method definition. But it’s easier to do it like this: if it doesn’t compile, it is easy for you to add the code you are TRYING to compile into your project, fix everything there, and then put it back into the automated test.




