Partager via


Unit Testing Activity CodeDOM Serializers

Windows Workflow Foundation (WF) defines workflows as object graphs. To save or compile workflow definitions, these object graphs must be serialized, and WF supports serialization to both XAML and code. Similar to other components, WF utilizes the .NET design-time framework for serialization and deserialization of objects. Serializing objects to code is done using CodeDomSerializer-derived classes.

In WF, the base CodeDOM serializer is ActivityCodeDomSerializer, and if you want to unit test your custom CodeDOM serializer for a custom activity, you can use the general approach I have earlier described. As it turns out, unit testing WF Activity CodeDOM serializers is simpler than unit testing components in general, since the base WF classes are already doing some of the work for you. You will still need to implement a designer loader for testing purposes, but you don't need to mess with a fake implementation of ITypeResolutionService.

Let's say that I wish to unit test my custom serialization logic for this activity and its corresponding serializer:

 [DesignerSerializer(typeof(MyActivityCodeDomSerializer),
     typeof(CodeDomSerializer))]
 public class MyActivity : Activity
 {
     // Implementation goes here
 }
  
 public class MyActivityCodeDomSerializer : 
     ActivityCodeDomSerializer
 {
     // Implementation goes here
 }

As you can tell, I've omitted the implementations of both Activity and serializer, since these are not important for this example. Just like the general case, the first thing to do is to create a designer loader that only exists for unit testing purposes. However, instead of deriving from CodeDomDesignerLoader, this class should derive from WorkflowDesignerLoader:

 internal class FakeCodeDomDesignerLoader : WorkflowDesignerLoader
 { 
     public override string FileName
     {
         get { return "Fake"; }
     }
  
     public override TextReader GetFileReader(string filePath)
     {
         throw new NotImplementedException();
     }
  
     public override TextWriter GetFileWriter(string filePath)
     {
         throw new NotImplementedException();
     }
 }

WorkflowDesignerLoader is an abstract class, so it's necessary to override its abstract members, but as you can tell, I haven't done much in that regard. In this case, GetFileReader and GetFileWriter is never called, and although FileName is invoked, the return value is not used for anything that affects the unit test, so I'm just returning a hard-coded string.

When you call BeginLoad on a DesignSurface with this loader (which I'll do in the unit test), PerformLoad will be called, so I have overridden this method to define a default root component:

 protected override void PerformLoad(
     IDesignerSerializationManager serializationManager)
 {
     base.PerformLoad(serializationManager);
  
     CompositeActivity rootActivity = new SequenceActivity();
     this.AddActivityToDesigner(rootActivity);
 }

In this case, the root component will always be a SequenceActivity; this corresponds to opening a new sequential workflow in the Visual Studio designer. You can now add other Activities to the root sequential workflow from the unit test, as I will demonstrate later.

To retrieve the serialized code, the designer loader must be flushed. While flushing, PerformFlush is called, so I override this method to capture the serialized code and save it in a private field:

 private CodeStatementCollection codeStatements_;
  
 protected override void PerformFlush(
     IDesignerSerializationManager serializationManager)
 {
     base.PerformFlush(serializationManager);
  
     ActivityCodeDomSerializer serializer = 
         new ActivityCodeDomSerializer();
     this.codeStatements_ = 
         (CodeStatementCollection)serializer.Serialize(
         serializationManager, this.LoaderHost.RootComponent);
 }

To retrieve the serialized code from a unit test, I implement a simple method that returns the code statements (recall that calling Flush causes PerformFlush to be called):

 internal CodeStatementCollection GetCodeStatements()
 {
     this.Flush();
     return this.codeStatements_;
 }

With FakeCodeDomDesignerLoader in place, it's now fairly straightforward to write a unit test that exercises the custom serializer that's associated with MyActivity:

 [TestMethod]
 public void MyTestCase()
 {
     FakeCodeDomDesignerLoader loader = 
         new FakeCodeDomDesignerLoader();
  
     DesignSurface surface = new DesignSurface();
     surface.BeginLoad(loader);
  
     IDesignerHost host =
         (IDesignerHost)surface.GetService(typeof(IDesignerHost));
  
     MyActivity ma = 
         (MyActivity)host.CreateComponent(typeof(MyActivity),
         "ma");
     ((SequenceActivity)host.RootComponent).Activities.Add(ma);
  
     CodeStatementCollection codeStatements =
         loader.GetCodeStatements();
  
     // Perform validation of code statements
 }

As with the general approach, the first thing to do is to create an instance of the designer loader. Then I create a new DesignSurface which is going to host the SequenceActivity root component that I can then add an instance of MyActivity to. Calling BeginLoad on the DesignSurface caused PerformLoad to be called on the FakeCodeDomDesignerLoader instance, which then creates the SequenceActivity and adds it as the root component.

The next step is to retrieve an instance of IDesignerHost, which can then be used to create an instance of MyActivity. Creating MyActivity instances this way ensures that any designers, editors and serializers (in this case, MyActivityCodeDomSerializer) is loaded and initialized correctly.

The newly created MyActivity instance is then added to the root component, i.e. the sequential workflow.

Getting the serialized code statements is then as simple as calling GetCodeStatements. This returns a CodeStatementCollection instance, and you can then use the principles outlined in my former article to validate that your custom serializer created the expected code statements.

Comments

  • Anonymous
    May 31, 2007
    I am having a problem viewing the designview of any forms in the sutudio. I am getting => The designer loader has not been initialized yet. You may only call this method after initialization<=. Do you have any idea how to fix my stdio so I can get to the design view of the forms? I have reinstalled the stuidio with no success to the problem.

  • Anonymous
    June 01, 2007
    Hi Mehdad Did you install the WF additions for Visual Studio? Are you getting this error in all projects, or just in some projects? I've had an issue that sounds like yours, when I tried to use the WF designer in a unit testing project. Basically, it seems you can't have both in the same project.