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)

Bookmark and Share

Comments

Add comment


(Will show your Gravatar icon)

  Country flag

biuquote
  • Comment
  • Preview
Loading