Improved Testability in WF4

One of the big complaints with the version of Windows Workflow Foundation that shipped in .NET 3 and .NET 3.5 (WF3) was that it was very difficult to unit test workflows.  Unit testing workflows was not impossible, but it was very difficult and required a lot of code to setup the workflow runtime and manage your workflow.  Since unit testing workflows in WF3 was clumsy and awkward, many developers chose to either not unit test their workflows, or just avoid using WF3 all together.
I am very happy to see that Microsoft has improved on this weakness with Workflow Foundation 4.0 (WF4). 

The first change that improves testability is the concept of In/Out arguments for workflows / activities.  The use of In/Out arguments and variables greatly simplifies the flow of data in WF4.  It is a big improvement over the use of Dependency Properties in WF3.  In WF4, all workflows and activities can accept a number of In arguments and return a number of Out arguments.  There is also support for In/Out arguments.  A more complete description of In and Out arguments and variables in WF4 can be found on MSDN (http://msdn.microsoft.com/en-us/library/dd489456(VS.100).aspx.aspx)).

In this example, I have created a simple workflow called Battle that accepts one SuperHero (the Hero InArgument), and one Villain (the Villain InArgument).  SuperHero and Villain are simple class that I have defined.  Both SuperHeros and Villains have a single SuperPower.  My workflow simulates a battle between the Hero and the Villain, then returns the name of the winner (the Winner OutArgument).  I have added the In and Out arguments using the workflow designer in Visual Studio 2010.

In the workflow xaml definition, the arguments are described simply as:

  <x:Members>
    <x:Property Name="Hero" Type="p:InArgument(w:SuperHero)" />
    <x:Property Name="Villain" Type="p:InArgument(w:Villain)" />
    <x:Property Name="Winner" Type="p:OutArgument(x:String)" />
  </x:Members>

The second big improvement is in the workflow runtime itself.  In WF3, there was a central WorkflowRuntime that created and managed workflow instances for us. The central runtime provided a set of events (WorkflowCompleted, WorkflowTerminated, etc.) that would notify us when a workflow entered specific states.  The problem with the central runtime was that the events would notify us when any workflow was completed (we would often receive notification about workflows that we are interested in).  This approach seemed a little clumsy, and thankfully it has been greatly improved in WF4.  In WF4, the central WorkflowRuntime no longer exists.  Instead, we can directly create instances of our workflow (using the constructor for that specific workflow), set its input arguments, then use a WorkflowInstance object to run that individual workflow.  The workflow instance gives us options to run the workflow synchronously or asynchronously (this was very difficult in WF3), and provides a set of events (WorkflowCompleted, WorkflowTerminated, etc.) to notify us when that specific workflow enters specific states.  Since we are dealing with a single workflow instance, we no longer need to worry about being notified about other workflow instances.  I think this is a much more natural approach than WF3, and really improves the testability of our workflows.  Since we no longer need to worry about initializing the WorkflowRuntime and dealing with its complexities, our unit tests suddenly become a whole lot simpler.

For more information on the changes surrounding the WorkflowRuntime, check out
http://msmvps.com/blogs/theproblemsolver/archive/2009/06/23/the-new-windows-workflow-foundation-4-runtime.aspx.

To make things even easier, WF4 includes a useful static Invoke method on the WorkflowInvoker class.  This allows us to run a workflow synchronously using a single line of code.  Our workflow unit tests become very logic and natural now.  We create a workflow and add any number of InArugments and OutArguments to it.  In our unit test, we create an instance of our workflow by calling its constructor.  We can then set the InArugments by setting the properties that were generated for that workflow class.  Finally, we can call WorkflowInvoker.Invoke to run the workflow.  The Invoke method returns an IDictionary<string, object> containing all the OutArguments from the workflow. We can then test the outputs using Assert as we would in any other unit test.

The only clumsy piece here is the way WF4 has chosen to return the OutArguments. It is using a key/value pair dictionary which can be a error prone.  A more natural approach would be if I could just access the OutArgument properties on the instance of the workflow that I passed in to the WorkflowInvoker.Invoke method.  Who knows, maybe this will change for the final release.

Overall, the changes introduced in WF4 can really improve the testability of our workflows.  If you previously decided not to use WF3 because it was too hard to unit test your workflows, you should take a good look at WF4.

NOTE:  This information and code is based on Visual Studio 2010 Beta 1 and is subject to change.