Testing Code-Generated Code with NUnit, Temporary Files, Voodoo, Rubber Bands and Reflection
Here's a little technical content for y'all. I write this on my new iJoy as the wife watches Lifetime this New Year's Day.
We're into Unit Testing here and getting more so every day. We also do a lot of code generation as well, and struggled with how to test that the code generation works from end to end.
We annotate XSDs to describe our objects. We have an XSD "DOM" Adapter that makes XML Schema documents look more friendly to consumers. We then use CodeSmith to spin through the "DOM" and generate code. Then we need to compile the generated code and hope it's correct. It's kind of a drawn out process.
However, since these objects are so fundamental to everything else that we do, it's important to test their generation. These objects should compile, have the correct attributes, be create-able at runtime, and serialize correctly.
Here's how we run CodeSmith in the NUnit tests to generate into temporary .g.cs files. The ".g.cs" extension is our own invention, allowing us to keep track of what's generated and what's written by hand.
We store the XML Schemas that will be generated as embedded resources in the test assemblies. That makes the Unit tests self-contained. This embedding technique was described in an earlier post. We unfold the schemas into the temporary directory and create a Corillian CodeSmith CodeRunner. The CodeRunner is a wrapper that we've written around CodeSmith to allow us to more easily run CodeSmith templates and avoid shelling out to EXEs. All this is done in [TextFixtureSetup] and deleted in [TestFixtureTearDown].
[Test()]
public void WriteTestMessagesFile()
{
CodeSmithRunner.Runner runner =
new Corillian.CodeGeneration.CodeSmithRunner.Runner();
runner.LoadTemplate(templatePath);
GenerateSource(runner,outFileMessagesNamePath,schemaMessagesPath);
}
private void GenerateSource(CodeSmithRunner.Runner runner,
string outputPath, string schemaPathIn)
{
using(StreamWriter output = File.CreateText(outputPath))
{
//deal with the schema file
Corillian.CodeGeneration.XmlSchemaExplorer.Schema schema =
Corillian.CodeGeneration.XmlSchemaExplorer.Schema.Read(schemaPathIn);
if(schema == null)
throw new ApplicationException("Schema couldn't be loaded");
Corillian.CodeGeneration.XmlSchemaExplorer.SchemaType[] types = schema.Parse();
if(types == null || types.Length == 0)
throw new ApplicationException("No types loaded from schema");
Corillian.CodeGeneration.XmlSchemaExplorer.TypeCollection typeCol =
new Corillian.CodeGeneration.XmlSchemaExplorer.TypeCollection(types,schemaPathIn);
runner.SetProperty("SchemaTypeCollection",typeCol);
runner.SetProperty("TargetNamespace","Test.Types");
runner.GenerateOutput(output);
}
}
The test above loads the CodeSmith .CST template from templatePath and outputs the resulting C# into outFileMessagesNamePath from the XSD in schemaMessagesPath.
Now that we've got generated C# in a temporary file, and assuming (since no exceptions stopped the tests) it worked, we'll want to compile the generated code into an assembly.
private Assembly Compile(string scope, string[] outputPaths)
{
Assembly retVal = compiledAsssemblies[outputPaths[0]+scope] as Assembly;
if (retVal == null)
{
Microsoft.CSharp.CSharpCodeProvider prov =
new Microsoft.CSharp.CSharpCodeProvider();
ICodeCompiler comp = prov.CreateCompiler();
string[] asms = new string[]{"System.Xml.dll",
"System.Web.dll",
"Corillian.Voyager.Common.dll",
"Corillian.Voyager.ExecutionServices.Client.dll",
"Corillian.CodeGeneration.Templates.Test.dll"};
string assemblyFileName =
Path.GetFileNameWithoutExtension(outputPaths[0]) + "." + scope + ".dll";
CompilerParameters options = new CompilerParameters(asms,assemblyFileName,true);
options.GenerateInMemory = false;
CompilerResults res = comp.CompileAssemblyFromFileBatch(options,outputPaths);
if(res.Errors.HasErrors)
{
Assert.Fail(res.Errors[0].ToString());
}
retVal = res.CompiledAssembly;
compiledAsssemblies.Add(outputPaths[0]+scope,retVal);
}
Assert.IsNotNull(retVal);
return retVal;
}
This Compile method takes an array of files to compile and returns the compiled System.Reflection.Assembly. We use the CSharpCodeProvider to compile the code. This is much cleaner and makes better use of what's available to us than doing something so coarse as shelling out to run csc.exe and loading the assembly. It also allows us to better detect compile errors. Notice the Assert.File if the CompilerResults has errors. We also keep the assembly stored away in a hashtable just in case another test calls Compile with the same main generated file.
Now that we can generate code and compile generated code into an Assembly, we need to actually create/instantiate the newly generated object and confirm that it has the characteristics we expect.
[Test()]
public void CheckDerivationAcrossNamespaces()
{
Assembly assembly = Compile("UI",
new string[]{outFileMessagesNamePath,outFileUIOnlyPath,assemblyInfo});
Type testResponse = assembly.GetType("Test.Types.SomeTestResponseMessage",true,true);
object theTestMessage =
testResponse.InvokeMember("cctor",BindingFlags.CreateInstance,null,null,null);
Assert.IsNotNull(theTestMessage);
PropertyInfo[] props = testResponse.GetProperties();
bool SomeDerivedUser = false;
bool FooTypedUser = false;
foreach(PropertyInfo prop in props)
{
if (prop.PropertyType.Name == "SomeDerivedUser" && prop.Name == "SomeDerivedUser")
{
Assert.AreEqual("SomeBaseUser",prop.PropertyType.BaseType.Name);
object[] attrs = prop.GetCustomAttributes(typeof(TagRemapAttribute),false);
Assert.IsTrue(attrs.Length == 2);
SomeDerivedUser = true;
}
if (prop.PropertyType.Name == "SomeDerivedComplexUser" && prop.Name == "FooTypedUser")
{
Assert.AreEqual("SomeBaseUser",prop.PropertyType.BaseType.Name);
object[] attrs = prop.GetCustomAttributes(typeof(TagRemapAttribute),false);
Assert.IsTrue(attrs.Length == 2);
FooTypedUser = true;
}
}
Assert.IsTrue(SomeDerivedUser ,"Didn't find a ID object of type
SomeDerivedUser in the UI's SomeTestResponseMessage!");
Assert.IsTrue(FooTypedUser,"Didn't find a ID object of type
SomeDerivedComplexUser in the UI's SomeTestResponseMessage!");
}
Here we compile an assembly from generated code and call assembly.GetType() to get the particular Type we're interested in. Now it gets interesting. We call the constructor of our type with Type.InvokeMember("cctor") and get the instance object back.
It's important to note that we have NOW compile-time references to any of these objects. They never existed at the time we wrote the tests, so there's no casting or type coercion that can be done. Everything is dynamic, but the tests are still confirming that the expect steps occured successfully. The tests are aiming to test the lifecycle of our objects, even though the objects will not be instantiated using Reflection. It's a parallel, but valid, reality.
In this year, we need to determine that our SomeTestResponseMessage contains two properties of specific types, with specific names, with specific custom attributes and we Assert each step of the way. If somehow make a subtle change that affects this use case, we'll hear about it via Test Failures.
The test results for this suite are generated by NUnit into XML then styled into HTML and combined with the full Test Suite and emailed to the team. Here's the output for just this TextFixture shown at right.
The only thing in these examples that is specific to what Corillian is our use of CodeSmith. Certainly tests could be written for other Code Generators that produce domain objects. I was particularly happy that CodeSmith provided a programmatic interface to it's stuff that allowed Patrick to create the CodeSmithRunner. That combined with the Microsoft CSharpCodeProvider makes these tests very robust (read: not fragile) and self contained. I've said it before, but I'd much rather use the correct public types than shell out whenever possible. You might remember I applied similar prejudices when calling Cassini's object model rather than running the command line.
About Scott
Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.
About Newsletter
Comments are closed.
Contains(string), EndsWith(string), Named(string), OfType(Type) - Attributes Only, StartsWith(string) and WithBindingFlags(BindingFlags). When an event is raised you can access the discovered member via the OnDiscover EventArgs.
One of the uses I've found for it is very similar to your NUnit Test Case sample code above:
[Test]
public void NamedTest()
{
this.reflector.Assemblies.Load(this.assemblyToTest);
this.reflector.OnMethodDiscovered += new AssemblyReflector.MethodDiscoveredEventHandler(OnMethodDiscovered);
this.reflector.Methods.Named("MethodNameToFind");
Assert.AreEqual(1, this.discoveredMethods.Count);
this.reflector.OnMethodDiscovered -= new AssemblyReflector.MethodDiscoveredEventHandler(OnMethodDiscovered);
}
private void OnMethodDiscovered(object sender, MethodInfoEventArgs e)
{
this.discoveredMethods.Add(e.Method);
}
You can write more complex assertions by accessing the members stored in this.discoveredMethods.
You can find some more info about it at http://blogs.conchango.com/howardvanrooijen/archive/2005/01/09/783.aspx