Freigeben über


Towards Unit Testing Component Serializers

When writing complex components or controls, it is sometimes necessary to implement custom CodeDOM serialization of the control. If the code serialization logic is complex, it would be nice if it was possible to unit test this logic. It's not quite as easy as it may seem, but it can be done, which I will demonstrate in this post.

Consider a custom Windows Forms control:

 [DesignerSerializer(typeof(MyControlCodeDomSerializer),
    typeof(CodeDomSerializer))]

public partial class MyControl : UserControl
{
    // Implementation goes here...
}

The actual implementation of MyControl really isn't important for this example, and neither is the designer serializer. The point here is just that MyControl has a custom serializer that I want to unit test.

When you implement a CodeDOM serializer, you typically just override CodeDomSerializer.Serialize(IDesignerSerializationManager, object). Since the default implementation (the one used by Visual Studio) of IDesignerSerializationManager is System.ComponentModel.Design.Serialization.DesignerSerializationManager, and that class has a default constructor, it seems pretty straightforward to just new up an instance of DesignerSerializationManager and pass it to the Serialize method together with an instance of MyControl, and then do Asserts on the return value.

Unfortunately, this doesn't work if you rely on your serializer's base class (CodeDomSerializer) to perform most of the work for you, and you'll typically end up with a return value of null, even if the same code works well when used by Visual Studio.

The problem lies in the .NET designer framework's loosely couple service architecture. Most design-time classes use an IServiceProvider to retrieve an instance of a specific service, and fail gracefully if none could be located. This is what's happening in this case.

To overcome this issue, it's necessary to more fully simulate the design-time environment when running a unit test: We'll create a new DesignSurface and add a MyControl instance to that surface. However, to get at the serialized code, we'll need to add a CodeDomDesignerLoader to the DesignSurface. This is an abstract class, and there are no useful public implementations of it available in the .NET framework, so we'll have to write one ourselves. This may seem like a lot of work, but on the other hand, it's completely reusable code, so you can use that implementation over and over in different unit tests in different projects.

 internal class TestCodeDomDesignerLoader
    : CodeDomDesignerLoader
{
    private CodeCompileUnit compileUnit_;
    private CSharpCodeProvider codeDomProvider_;
    private TestTypeResolutionService typeRsSrvc_;
 
    internal TestCodeDomDesignerLoader()
        : base()
    {
        this.codeDomProvider_ = new CSharpCodeProvider();
        this.typeRsSrvc_ = new TestTypeResolutionService();
    }
 
    protected override CodeDomProvider CodeDomProvider
    {
        get { return this.codeDomProvider_; }
    }
 
    protected override CodeCompileUnit Parse()
    {
        DesignSurface ds = new DesignSurface();
        ds.BeginLoad(typeof(Form));
 
        IDesignerHost host =
            ds.GetService(typeof(IDesignerHost))
            as IDesignerHost;
        host.RootComponent.Site.Name = "Form1";
 
        CodeTypeDeclaration designedClass =
            new CodeTypeDeclaration
            (host.RootComponent.Site.Name);
        designedClass.BaseTypes.Add(typeof(Form));
 
        CodeNamespace ns =
            new CodeNamespace("TestNameSpace");
        ns.Types.Add(designedClass);
 
        CodeCompileUnit ccu = new CodeCompileUnit();
        ccu.Namespaces.Add(ns);
 
        return ccu;
    }
 
    protected override ITypeResolutionService
        TypeResolutionService
    {
        get { return this.typeRsSrvc_; }
    }
 
    protected override void Write(CodeCompileUnit unit)
    {
        this.compileUnit_ = unit;
    }
 
    internal string GetCode()
    {
        this.Flush();
 
        CodeGeneratorOptions options =
            new CodeGeneratorOptions();
        options.BracingStyle = "C";
 
        using (StringWriter sw = new StringWriter())
        {
            this.codeDomProvider_.
                GenerateCodeFromCompileUnit
                (this.compileUnit_, sw, options);
            return sw.ToString();
        }
    }
 
    internal object GetServiceInternal(Type serviceType)
    {
        return this.GetService(serviceType);
    }
}

Most of the class is pretty straightforward. The most notable parts are the Parse method, the TypeResolutionService property and the GetCode method.

The Parse method is probably the most complex to understand. When a DesignSurface is instructed to begin loading (as we will do later, in the unit test) it will (among many other things) call the Parse method to load the initial design surface (imagine you had a Windows Form which you already saved and now want to load again). This particular Parse implementation just assumes that you have an initial empty Form, so it creates the necessary CodeDOM objects which corresponds to the code which will create the Form1 class (which derives from Form).

The DesignSurface will use this code to load Form1 into the designer, so you can add more controls (specifically, in this case, MyControl) to it. This is basically similar to what Visual Studio does.

The GetCode method is just a convenience method to create a string from a CodeCompileUnit, and is only meant to be used from the unit test. Calling Flush on the loader causes the Write method to be called with a CodeCompileUnit representing the current state of the DesignSurface (including any controls added to the surface).

The TypeResolutionService property requires a bit more explanation than it may seem to warrant if you just look at it casually. It returns an implementation of ITypeResolutionService, and like with the CodeDomDesignerLoader abstract class, the .NET framework includes no public implementations of this interface. This is the other helper class we need to implement to get things working.

The ITypeResolutionService is used to locate types and assemblies by name. The Designer framework will query this service with a type name to retrieve a Type instance. The name which is used is sometimes an assembly-qualified type name, including assembly, version number, public key token, etc. - in which case Type.GetType can be used. In other cases, however, the name supplied is just a type name with full namespace path, but no assembly information.

For testing purposes, the ITypeResolutionService interface can be implemented like this:

 internal class TestTypeResolutionService
    : ITypeResolutionService
{
    private Dictionary<string, Type> cachedTypes_;
 
    internal TestTypeResolutionService()
    {
        this.cachedTypes_ = new Dictionary<string, Type>();
    }
 
    #region ITypeResolutionService Members
 
    public Assembly GetAssembly(AssemblyName name,
        bool throwOnError)
    {
        throw new NotImplementedException();
    }
 
    public Assembly GetAssembly(AssemblyName name)
    {
        throw new NotImplementedException();
    }
 
    public string GetPathOfAssembly(AssemblyName name)
    {
        throw new NotImplementedException();
    }
 
    public Type GetType(string name, bool throwOnError,
        bool ignoreCase)
    {
        AssemblyName[] assemblyNames =
            Assembly.GetExecutingAssembly().
            GetReferencedAssemblies();
        foreach (AssemblyName an in assemblyNames)
        {
            Assembly a = Assembly.Load(an);
            Type[] types = a.GetTypes();
            foreach (Type t in types)
            {
                if (t.FullName == name)
                {
                    this.cachedTypes_[name] = t;
                    return t;
                }
            }
        }
 
        return Type.GetType(name, throwOnError,
            ignoreCase);
    }
 
    public Type GetType(string name, bool throwOnError)
    {
        return this.GetType(name, throwOnError, false);
    }
 
    public Type GetType(string name)
    {
        return this.GetType(name, true);
    }
 
    public void ReferenceAssembly(AssemblyName name)
    {
        throw new NotImplementedException();
    }
 
    #endregion
}

In the current unit testing scenario, assembly information is never requested, so I've just left the assembly-related methods to throw a NotImplementedException if called. If you ever encounter a situation where your test code causes some of these methods to be called, you will have to implement those methods in addition to the type-related methods.

The only code of interest here is the GetType(string, bool, bool) method. It simply loops through all referenced assemblies, searching for a type with a matching name in each assembly. This is a quite expensive operation (there are lots of types), so you will see CPU utilization going to 100% for a little while when this is happening. My weak attempt at optimization (a dictionary of cached types) is not very effective, since an instance of the class only exists for the duration of a single test (I could have used a static dictionary, or a cached instance of TestTypeResolutionService across all tests, but that would violate the principle that each unit test should be fully independent).

With the TestCodeDomDesignerLoader and TestTypeResolutionService classes ready, writing the unit test itself is relatively straightforward:

 [TestMethod]
public void SerializeBasicMyControl()
{
    TestCodeDomDesignerLoader loader =

        new TestCodeDomDesignerLoader();
 
    DesignSurface surface = new DesignSurface();

    surface.BeginLoad(loader);

    IDesignerHost host =

        surface.GetService(typeof(IDesignerHost))

        as IDesignerHost;
 
    MyControl mc =

        host.CreateComponent(typeof(MyControl))

        as MyControl;
 
    ((Form)host.RootComponent).Controls.Add(mc);
 
    string serializedCode = loader.GetCode();
 
    // Compare against expected code string here...
}

An instance of TestCodeDomDesignerLoader (which contains an instance of TestTypeResolutionService) is created and added to a new DesignSurface using the BeginLoad method. This will, as I wrote above, cause TestCodeDomDesignerLoader.Parse to be called. The code from the Parse method is used to set up the DesignSurface, so with this particular implementation, the design surface now contains an instance of the Form1 class (which derives from Form).

It is now possible to use the designer's IDesignerHost implementation to create a new instance of the MyControl class. In this example, creating an instance of the class is all I do, but in more advanced tests, I could then begin to set properties and call methods on the control.

Creating the control using IDesignerHost ensures that the control is sited in an environment with all the required services. It is also necessary to add the control to the root component, which in this case is a Form (actually Form1, since that's what I defined in the Parse method).

The last thing to do is call the GetCode convenience method on the designer loader to get a string with all the code.

At this point, I would compare that string with a target string which I've read from a target code file in my test project, but I'll leave that as an exercise to the reader.

This may seem like an overwhelmingly large amount of work to do, but remember that you only ever need to write TestCodeDomDesignerLoader and TestTypeResolutionService once, and you can just copy them from here; notice that I've omitted tons of using statements for clarity, so you'll need to add those yourself, but I'm sure Visual Studio 2005 will be very helpful in this regard. A lot of the classes involved are defined in System.Design, so you will need to add a reference to that assembly.

In my example, the two reusable classes are internal, which doesn't make them that reusable after all. It would be obvious to create them as public classes and put them in a common unit testing utility library.

There are some weak spots in the code here and there. For once, I haven't handled the assembly-related methods in TestTypeResolutionService. Additionally, the GetType method could probably do with a bit of optimization. There may be other areas which need improvement, which is what I've attempted to hint at in the title. This is not the definitive, authoritative article on this subject, but it will get you started.

If your control serialization code is of only intermediate complexity, you will be very happy to have a suite of unit tests which can be used for quality assurance.

Update: My new post about a faster TestTypeResolutionService explains how to improve test performance.

Comments

  • Anonymous
    February 18, 2006
    The comment has been removed

  • Anonymous
    March 27, 2006
    In February, I wrote about unit testing CodeDomSerializers, but although it was a good initial attempt,...

  • Anonymous
    September 16, 2006
    After two days serch I found this excelent
    blog. It helped me realy very much.
    I have to generate Dialogs from a non-dot.net
    System.
    This Unit-Testsystem inspirated me to som new Ideas.
    Thank you.

  • Anonymous
    September 17, 2006
    Hi Manfred

    Good to hear the article helped you :)

    Thank you for your feedback.

  • Anonymous
    April 01, 2007
    The comment has been removed

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

  • Anonymous
    August 08, 2007
    I had a hard time trying to persist forms with the CodeDomDesignerLoader. Thanks to you, I did it!

  • Anonymous
    August 08, 2007
    Hi joci Thank you for letting me know - it's always nice to know that this was useful to someone :)

  • Anonymous
    December 26, 2007
    Hi, Can I get the source code for persisting forms with CodeDOMDesignerLoader? Could you please let me know? Thanks in advance, Anantha

  • Anonymous
    December 27, 2007
    The comment has been removed

  • Anonymous
    January 14, 2009
    Very useful!  This will save a lot of time testing for sure...

  • Anonymous
    January 14, 2009
    Hi Nick Thank you - I'm glad you found the post useful.