In the project I am currently working on we make use of the
Windows Workflow Foundation (WF), which is a part of the .NET Framework 3.0.
We are using it for some potentially long running workflows in an application
that is importing information from files to a backend system. The communication
channel to the backend system is (from various well-founded reasons) mail. So
when importing a piece of information, our application will send a mail to a
certain mail address. The backend system will (hopefully) pick up the mail,
parse it, and import the information into its database, and then send a
confirmation mail back to our application. When that confirmation mail comes
back, the information is regarded as imported.
So, we are using WF to host a workflow taking care of this process. And of course
we want to unit test our workflows. I guess I should point out that with the
amount of code that is touched by these tests, they might not be considered as
unit tests in their purest form. I like to think about them as some sort of
integration tests, driven by the unit testing framework (NUnit),
even though we do mock away some parts (such as the mail communication). That
said, consider the following code:
[Test]
public void CanExecuteCreateFileWorkflow_Normal()
{
string expectedText = "Text from unit test";
// get workflow runtime from a hosting class
WorkflowRuntime runtime = Host.Instance.Runtime;
WorkflowInstance instance = runtime.CreateWorkflow(typeof(CreateFile));
instance.Start();
// Verify the result
string textInFile = File.ReadAllText("file_from_workflow.txt");
Assert.AreEqual(expectedText, textInFile);
}
While the above test would certainly run, chances are that it would fail. Unit
testing workflows presents a few challenges, and I will focus on two of them
here. First, the workflow runtime maintains its own threadpool from which it
allocates threads on which the workflow instances are executed. Second,
depending on your workflow design, the workflow might very well get "stuck" in
an idle state, waiting for input.
Let's take care of the second point first. When running unit tests on a workflow,
I am mainly interested in two things: how the workflow ends, and verifying data
that the workflow has operated on. Let's start off by finding out whether the
workflow completes in a proper fashion.
For this article I will use a small, rather meaningless workflow for
demonstration. The workflow itself is very simple; it will create a text file on
disk. But it serves its purpose in showing how you can unit test a workflow that
includes a scenario where the workflow waits for input from the outside.
During the time that a workflow instance is hosted by a
WorkflowRuntime object, the runtime will raise a number of
events in order to inform about state changes in the workflow. There are 11
such events, but I will focus here on those that I use most for unit testing
purposes:
WorkflowCompleted,
WorkflowIdled and
WorkflowTerminated. WorkflowCompleted occurs when a
workflow has completed in a normal way, WorkflowIdled occurs
when the workflow enters an idle state, such as when listening for an external
event, and WorkflowTerminated occurs when a workflow is
terminated, which can be caused by an exception being thrown while it executes.
In order to listen to those events, and let them exchange information with the
unit test, I use
anonymous methods.
Let's add the event listener methods to the unit test code listed above:
[Test]
public void CanExecuteCreateFileWorkflow_Normal()
{
string expectedText = "Text from unit test";
// get workflow runtime from a hosting class
WorkflowRuntime runtime = Host.Instance.Runtime;
WorkflowInstance instance = runtime.CreateWorkflow(typeof(CreateFile));
// let's assume that the workflow did not complete gracefully, until we
// have some proof
bool workflowCompletedNormally = false;
// this is the event handler for the WorkflowCompleted event. This event is raised
// when the workflow completes in a normal manner
runtime.WorkflowCompleted += delegate(object sender, WorkflowCompletedEventArgs e)
{
// workflow completed normally
workflowCompletedNormally = true;
};
// Workflow terminated handler. This one is invoked if the workflow terminates in
// an unexpected way (because of an exception for instance).
runtime.WorkflowTerminated += delegate(object sender, WorkflowTerminatedEventArgs e)
{
// workflow was terminated
};
instance.Start();
// Verify that the workflow completed normally
Assert.IsTrue(workflowCompletedNormally);
// Verify the result
string textInFile = File.ReadAllText("file_from_workflow.txt");
Assert.AreEqual(expectedText, textInFile);
}
Now we have a way of determining whether the workflow ends in a normal manner or
not. But we have one big problem. In the code example above, by the time that
the test performs the result verification, it's likely that the workflow is
still executing on its own thread. Depending on how long time it takes to
finish, it may very well continue to happily run on its own for some time after
the unit test method is finished. We obviously need another approach than the
code above.
In order to give the workflow time to finish executing, and for the events to be
raised, we need to let the unit test code wait for a while, before it finishes.
In order to do this we can use an
AutoResetEvent class instance. That will put the thread that is executing
the unit test in a waiting state, allowing the workflow thread to do its job.
Then the event listeners can be called, and when the workflow is either
completed or terminated, the event listener method that is invoked can call the
Set method on the AutoResetEvent object, and the unit test can finish
executing.
[Test]
public void CanExecuteCreateFileWorkflow_Normal()
{
string expectedText = "Text from unit test";
AutoResetEvent waitHandle = new AutoResetEvent(false);
// get workflow runtime from a hosting class
WorkflowRuntime runtime = Host.Instance.Runtime;
WorkflowInstance instance = runtime.CreateWorkflow(typeof(CreateFile));
// let's assume that the workflow did not complete gracefully, until we
// have some proof
bool workflowCompletedNormally = false;
// this is the event handler for the WorkflowCompleted event. This event is raised
// when the workflow completes in a normal manner
runtime.WorkflowCompleted += delegate(object sender, WorkflowCompletedEventArgs e)
{
// workflow completed normally
workflowCompletedNormally = true;
waitHandle.Set();
};
// Workflow terminated handler. This one is invoked if the workflow terminates in
// an unexpected way (because of an exception for instance).
runtime.WorkflowTerminated += delegate(object sender, WorkflowTerminatedEventArgs e)
{
// workflow was terminated
waitHandle.Set();
};
instance.Start();
// Wait around here for a while until the workflow is ready. Let's give it 5 seconds.
waitHandle.WaitOne(5000, false);
// Verify that the workflow completed normally
Assert.IsTrue(workflowCompletedNormally);
// Verify the result
string textInFile = File.ReadAllText("file_from_workflow.txt");
Assert.AreEqual(expectedText, textInFile);
}
Now we have a setup where we can start to investigate the result of the workflow,
but there is one last thing we need to change to make this work in all
situations. Can you spot it? Well, it may not be obvious; the problem is that it
is likely that all your unit tests will make use of the same WorkflowRuntime
instance. As the code above looks, the unit test will hook up event listeners,
but not release them again. The WorkflowRuntime events will gather a larger
audience for each unit test that is executed. As our test looks above, not much
will happen, but if we for instance use the WorkflowIdled event to raise events
that the workflow listen to, in order to drive the workflow forward, we might
get unexpected results when event listeners from already executed unit tests
respond. Luckily the solution is simple: instead of assigning the anonymous
methods directly to the events, we assign them to variables. That way we can
attach and detach them to the events in a proper manner:
[Test]
public void CanExecuteCreateFileWorkflow_Normal()
{
string expectedText = "Text from unit test";
AutoResetEvent waitHandle = new AutoResetEvent(false);
// get workflow runtime from a hosting class
WorkflowRuntime runtime = WorkflowHost.Runtime;
WorkflowInstance instance = runtime.CreateWorkflow(typeof(CreateFile));
bool workflowCompletedNormally = false;
EventHandler<WorkflowEventArgs> WorkflowIdledHandler = delegate(object sender, WorkflowEventArgs e)
{
// get the ICreateFileService instance from the runtime
ICreateFileService service = WorkflowHost.Runtime.GetService<ICreateFileService>();
// set the desired file content
service.SubmitText(instance.InstanceId, expectedText);
};
// this is the event handler for the WorkflowCompleted event. This event is raised
// when the workflow completes in a normal manner
EventHandler<WorkflowCompletedEventArgs> WorkflowCompletedHandler = delegate(object sender, WorkflowCompletedEventArgs e)
{
// workflow completed normally
workflowCompletedNormally = true;
waitHandle.Set();
};
// Workflow terminated handler. This one is invoked if the workflow terminates in
// an unexpected way (because of an exception for instance).
EventHandler<WorkflowTerminatedEventArgs> WorkflowTerminatedHandler = delegate(object sender, WorkflowTerminatedEventArgs e)
{
// workflow was terminated
waitHandle.Set();
};
// Hook up event listeners
runtime.WorkflowCompleted += WorkflowCompletedHandler;
runtime.WorkflowTerminated += WorkflowTerminatedHandler;
runtime.WorkflowIdled += WorkflowIdledHandler;
instance.Start();
// Wait around here for a while until the workflow is ready. Let's give it 5 seconds.
if (!waitHandle.WaitOne(5000, false))
{
Assert.Fail("Test timed out.");
}
// Release event listeners
runtime.WorkflowCompleted -= WorkflowCompletedHandler;
runtime.WorkflowTerminated -= WorkflowTerminatedHandler;
runtime.WorkflowIdled -= WorkflowIdledHandler;
// Verify that the workflow completed normally
Assert.IsTrue(workflowCompletedNormally);
// Verify the result
string textInFile = File.ReadAllText("file_from_workflow.txt");
Assert.AreEqual(expectedText, textInFile);
}
There we have a working template for using NUnit to run automated tests on
workflows. In our case we have our workflow tests (as well as all other unit
tests) executed as part of our build script, so that we detect breaking changes
as early as possible.
I have prepared a sample project containing the simple workflow, a simple workflow
host, the unit test and a console application that will run the workflow in the same
way as the unit test does. You can download
it here: Unit-Testing-Workflows.zip (313.83 kb)