As we move into the SOA world workflows will become more common and so the need to test them will increase. If we take the most simplistic view these are a sets if statements and loops so should be amenable to automated testing and TDD.
However you have to careful how you try to do this, it is too easy to forget that your workflow will run the the WF thread. Why is this problem?
Consider this scenario, if you create a simple sequential WF console application you get a block of code like this in the Main(args) method
using (WorkflowRuntime workflowRuntime = new WorkflowRuntime())
{
AutoResetEvent waitHandle = new AutoResetEvent(false);
workflowRuntime.WorkflowCompleted += delegate(object sender, WorkflowCompletedEventArgs e)
{
// could put asserts here (A)
waitHandle.Set();
};
workflowRuntime.WorkflowTerminated += delegate(object sender, WorkflowTerminatedEventArgs e)
{
// could put asserts here (B) waitHandle.Set();
};
WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(WorkflowConsoleApplication1.Workflow1));
instance.Start();
waitHandle.WaitOne();
}
// could put asserts here (C)
This block of code will be the basis of each of your unit tests, but as indicated you could put your test asserts at one of three places
- In the success delegate
- In the fail/terminate delegate
- After the workflow has completed
The best place is the third option, when the workflow has finished, even though you know in a test whether it should completed OK or failed. The problem with the previous two locations is that the anonymous delegate will be run in the WF worker thread. Interestingly, if the assert passes in this location all will be OK, but if it fails the thrown assert exception (the mechanism used in nUnit to trap failure and in other test frameworks to my knowledge) will be in the wrong thread and so will not be picked up by the test harness, so in effect the test runner stalls waiting for completion or an exception, neither of which arrive. This issue is not the the case if the asserts are done when the workflow completes. This does mean that you have to pass results out of the workflow, but this should not be a major issue as the results should be simple objects (workflow parameters) or checks on external (mock?) systems, such as file systems, DBs or Smtp servers.
Though not always standard practice in TDD, I think it is a good idea here to move the code in the above sample’s using statement into a static method in your test class; it is boiler plate that will probably be use in many tests. As tests are meant to readable I think a single line call to the RunWorkFlow() method is better than a block of repeated loop and delegate code. Remember you do have to be careful here, as you may need to pass in parameters and get a return value to get the end state of the workflow; the requirement will depend what your workflow does and how it interacts with other systems. It maybe you need more than one version of the RunWorkFlow() static method depending on what is under test e.g one to harvest output parameters from completed workflow runs and another to harvest inner exceptions that are inside the workflow terminated delegate.
So now there is not reason to not test your WF workflows – assuming you can mock out any external connections, and if you are using web services or IoC to talk to data stores this should be fairly easy.
Next I am off to try to do the same with SharePoint workflows.