次の方法で共有


Validating Code Serialized by CodeDomDesignerLoaders

A while back I wrote about unit testing component serializers, but I left the task of validating the generated code against an expected result up to the reader. The approach outlined in that article was to retrieve the generated code as a string and compare it to an expected string. While this is certainly a relatively easy and intuitive approach, experience has meanwhile taught me that it leaves something to be desired with regards to robustness.

Let's revisit the original example from the former article, where I'm serializing a control called MyControl. The generated code typically looks something like this:

 //------------------------------------------------------------------------------
 // <auto-generated>
 //     This code was generated by a tool.
 //     Runtime Version:2.0.50727.312
 //
 //     Changes to this file may cause incorrect behavior and will be lost if
 //     the code is regenerated.
 // </auto-generated>
 //------------------------------------------------------------------------------
  
 namespace TestNamespace
 {
  
  
     public class Form1 : System.Windows.Forms.Form
     {
  
         private ReadOnlyCollectionSerialization.MyControl myControl1;
  
         private Form1()
         {
             this.InitializeComponent();
         }
  
         private void InitializeComponent()
         {
             this.myControl1 = new ReadOnlyCollectionSerialization.MyControl();
             this.SuspendLayout();
             // 
             // myControl1
             // 
             this.myControl1.Location = new System.Drawing.Point(0, 0);
             this.myControl1.Name = "myControl1";
             this.myControl1.Size = new System.Drawing.Size(150, 150);
             this.myControl1.TabIndex = 0;
             // 
             // Form1
             // 
             this.ClientSize = new System.Drawing.Size(284, 264);
             this.Controls.Add(this.myControl1);
             this.Name = "Form1";
             this.ResumeLayout(false);
         }
     }
 }

There are a couple of potential issues with performing a strict string comparison against this expected code:

  • The version of the .NET framework appears in the initial comment. As I discovered when I moved to the x64 platform, the build and revision numbers are not the same as on x86, which suddenly caused my unit tests to fail.
  • In principle, this form of testing is very sensitive to white spaces. When you compare two strings, white spaces count, but in code, a lot of the white space doesn't matter. Since you essentially care about the functionality of the serialized code, and not the white space, such a strict comparison is not warranted. Although I must admit that this has never been an issue for me, this has always bothered me. What if a future service pack for .NET changes the serialization behavior such that there's only one, and not two, empty lines between the namespace declaration and the type declaration in the above example? Surely, this would have no impact on the functionality of the serialized code, but it would cause a string-comparing test to fail.

A more robust approach would be to validate against the CodeDom itself. To do this, I first need to add a new method to my TestCodeDomDesignerLoader class (I'll refer you to my original article for an introduction to this class):

 internal CodeCompileUnit GetCodeCompileUnit()
 {
     this.Flush();
  
     return this.compileUnit_;
 }

Instead of serializing the code using a CodeDomProvider, I simply return the CodeCompileUnit generated by the serializer. It's now possible to test against this CodeCompileUnit, and I'll show you how by walking you through a continuation of the example from my original article. In that example, the test SerializeBasicMyControl calls loader.GetCode to get the code as a string. Instead of doing that, I'll retrieve the CodeCompileUnit:

 CodeCompileUnit compileUnit = loader.GetCodeCompileUnit();

The compileUnit variable will now contain a complete, language-neutral representation of the code. If the serializer works correctly, this code should correspond to the auto-generated code above. It even contains the comments, but since these have no impact on functionality, I'll ignore them, so the first thing I need to test is that the namespace is called TestNamespace, and there's only a single namespace in the CodeCompileUnit:

 CodeNamespaceCollection namespaces = compileUnit.Namespaces;
 Assert.AreEqual<int>(1, namespaces.Count);
 Assert.AreEqual<string>("TestNamespace", namespaces[0].Name);

The next thing to validate is that there's only a single type in the code, and that it's called Form1:

 CodeTypeDeclarationCollection types = namespaces[0].Types;
 Assert.AreEqual<int>(1, types.Count);
 Assert.AreEqual<string>("Form1", types[0].Name);

Besides validating the name of the generated type, it's also relevant to validate that Form1 derives from Form, and that it doesn't derive from other types (i.e. implement any interfaces):

 CodeTypeReferenceCollection baseTypes = types[0].BaseTypes;
 Assert.AreEqual<int>(1, baseTypes.Count);
 Assert.AreEqual<string>("System.Windows.Forms.Form",
     baseTypes[0].BaseType);

At this point, I could choose to continue with validating the the code contains a private field called myControl1, that the Form1 constructor is defined, and that it calls the InitializeComponent method, but I'll skip that for now, since the most relevant part to test is the InitializeComponent method itself. To validate that method, I must ensure that it exists and get a reference to it. There are three members of Form1: the myControl1 field, the constructor, and the InitializeComponent method, so it's necessary to loop through the type's members to find it:

 CodeTypeMemberCollection members = types[0].Members;
CodeMemberMethod method = null;

foreach (CodeTypeMember member in members)
{
    method = member as CodeMemberMethod;
    if ((method != null) && 
        (method.Name == "InitializeComponent"))
    {
        break;
    }
    method = null;
}
Assert.IsNotNull(method);

Now that I have the method definition, I also have all the code statements it contains:

 CodeStatementCollection statements = method.Statements;

The validation that has occurred until this point validates a part of the serialization code that typically doesn't have anything to do with what you are trying to test. In this example, the purpose of the test is to validate the custom serializer that serializes instances of MyControl; the relevant code will be placed within the InitializeComponent method. The namespace, name of the Form, etc. is actually defined in the TestCodeDomDesignerLoader's Parse method, so you might consider refactoring all of this validation code into a method of TestCodeDomDesignerLoader, and let it return the CodeStatementCollection from the InitializeComponent method. This would allow you to focus on the essential part of each test case.

In this test, I'll just validate that the first serialized line of code is as expected, to give you an idea of how this can be done. This line of code should be an assignment, where a new instance of MyControl is assigned to the myControl1 field:

 Assert.IsInstanceOfType(statements[0], typeof(CodeAssignStatement));
 CodeAssignStatement assignStatement = (CodeAssignStatement)statements[0];

This first part just checks that the statement is an assignment. Next, I'll validate that the left side of the assignment is a reference to this.myControl1:

 Assert.IsInstanceOfType(assignStatement.Left,
     typeof(CodeFieldReferenceExpression));
 CodeFieldReferenceExpression fieldReference =
     (CodeFieldReferenceExpression)assignStatement.Left;
 Assert.AreEqual<string>("myControl1", fieldReference.FieldName);
 Assert.IsInstanceOfType(fieldReference.TargetObject,
     typeof(CodeThisReferenceExpression));

The right side of the expression is a bit more complex, since I need to validate that the expression is creating a new instance, and that the created instance is of type MyControl. It's easy to validate that the expresion creates a new instance:

 Assert.IsInstanceOfType(assignStatement.Right,
     typeof(CodeObjectCreateExpression));
 CodeObjectCreateExpression objectCreate =
     (CodeObjectCreateExpression)assignStatement.Right;

To validate that a MyControl instance is created using the default constructor (i.e. that no parameters are used in the constructor), I add this:

 Assert.IsInstanceOfType(objectCreate.CreateType,
     typeof(CodeTypeReference));
 CodeTypeReference codeType = 
     (CodeTypeReference)objectCreate.CreateType;
 Assert.AreEqual<string>("ReadOnlyCollectionSerialization.MyControl",
     codeType.BaseType);
 Assert.AreEqual<int>(0, objectCreate.Parameters.Count);

This is quite a lot of validation code to verify a single line of code!

As I started out by stating, validating against a string is a far more intuitive approach, but the one outlined here is more robust. Fortunately, the large portion of the validation code that occurs before the validation of InitializeComponent can be reused across multiple test cases.

Additionally, just because you could, in theory, validate every single statement in the InitializeComponent method, it doesn't mean that you should. Typically, when you unit test component serializers, you only care about specific lines of code, since most of the generated code is rather trivial. If this is the case, you can skip all the trivial statements and find only those you care about, and then validate those.

Comments

  • Anonymous
    April 05, 2007
    Windows Workflow Foundation (WF) defines workflows as object graphs. To save or compile workflow definitions,