Foundations

Unit Testing Workflows And Activities

Matt Milner

Code download available at:MSDN Code Gallery(168 KB)

Contents

Testing Considerations
Testing Activities
Testing Rules
Testing Runtime Services
Testing Workflows

As an author and instructor, I often get asked questions about how to test Windows Workflow Foundation (Windows WF) components. Part of learning to use a new framework is figuring out how to incorporate the tools and components into your development process. This month I will be discussing some of the challenges and techniques related to testing activities, workflows, and associated components. This is in no way intended to be a best practice guide for unit testing or test-driven development. Rather, it is meant to provide you with the techniques you can use to test your workflows in your preferred development methodology.

Testing Considerations

When writing unit tests, you need to test common paths through your code, but things are never quite that simple. In addition to the "happy path" or the expected execution path, you also must test exception paths. In addition, many items to be tested have dependencies, and it would be unrealistic to test through them all. For example, a business object may rely on a data access layer. In testing, however, rather than relying on the database, it is common to use mock objects—objects that have the same interface as the data access layer but do not access the database at run time.

These same issues are present with workflow, but in some cases the approach is slightly different. In other cases, just the fact that Windows WF is a new technology can cause some developers difficulty in deciding how to test their code.

In addition to typical testing issues, using a framework like Windows WF introduces other complexities. Because Windows WF is based on a runtime that manages the execution of workflows and activities, testing must, in almost all cases, involve the use of the runtime. Many of the classes involved in the runtime execution are sealed or otherwise protected, so mocking those objects becomes nearly impossible. In addition, it would be difficult to ensure that any mock objects you created provided all of the necessary functionality found in the runtime. For those reasons, testing workflows or activities generally requires the runtime.

Using the workflow runtime introduces several interesting challenges. First, because the runtime can use different schedulers, each with its own threading model, unit tests must be written with a particular threading model. The best scheduler for testing may not be the same as the scheduler that best fits the ultimate host for your workflows.

The runtime provides services for developers that are helpful at run time but can make testing more challenging—exception handling, for example. The runtime catches all exceptions and manages them in the workflow. This means testing for expected exceptions will work a bit differently than in standard Microsoft .NET Framework code.

When testing a solution built on Windows WF, there are several components that generally need testing: activities, workflows, rules and custom runtime services. Throughout the rest of this article, I will cover the common issues developers run into when testing these components and propose techniques for addressing them.

Testing Activities

When testing activities, it is important to be able to do so in isolation, providing inputs and testing outputs. Rather than testing activities in the context of a workflow, activities should be tested as individual components. Fortunately, in Windows WF, all workflows are simply activities. And that means the runtime can execute any activity as a workflow, even something as simple as an e-mail activity.

Before executing the activity, the runtime must be set up and the appropriate scheduler and other runtime services added to it. Generally, when testing activities, the ManualWorkflowScheduler­Service is easiest to use, as it allows the activity to execute on the same thread as the unit test and provides the most control over the execution. The scheduler does not do any work in the workflow or activities until the host, in this case the test, provides the thread to do the work.

When testing workflows, I typically use test setup and teardown methods to initialize the runtime and gracefully shut it down. In the test methods, I can focus on executing a particular activity test knowing that I have a clean runtime in which to execute the test. Figure 1 shows code for a simple activity test that uses the functionality in Windows WF of passing parameters to a workflow to initialize it.

Figure 1 Executing an Activity as Workflow

Dictionary<string, object> results = null;
Exception ex = null;

runtime.WorkflowCompleted += delegate(
  object sender, WorkflowCompletedEventArgs wce) {
  results = wce.OutputParameters; };

Dictionary<string, object> wfParams = new Dictionary<string, object>();
wfParams.Add("To", "mmnet30@microsoft.com");
wfParams.Add("From", "donotreply@example.org");
wfParams.Add("Subject", "Unit testing");

WorkflowInstance instance = runtime.CreateWorkflow(
  typeof(WFComponents.SendMailActivity), wfParams); 
instance.Start();

ManualWorkflowSchedulerService man = 
  runtime.GetService<ManualWorkflowSchedulerService>();
man.RunWorkflow(instance.InstanceId);

//the workflow is done, now we can test the outputs
Assert.IsNotNull(results, "No results found");
Assert.AreEqual<string>(wfParams["To"].ToString(), 
  results["To"].ToString(), "To address was changed");

There are several things to notice in this code. First, the SendMailActivity is used as the definition of the workflow and executed independent of any containing workflow. Parameters are passed to the activity using standard Windows WF techniques where a dictionary of values is passed to the runtime and mapped to the properties on the activity. The manual scheduler is employed to execute the workflow, and, therefore, the RunWorkflow method is used when the activity is ready to be executed. Once the RunWorkflow method has completed, the simple activity has finished and the output parameters can be extracted and assertions made.

Notice that the output parameters are collected during the WorkflowCompleted event and the reference mapped to a local variable that can then be inspected. Because the manual scheduler is being used, the WorkflowCompleted event execution happens on the same thread as all other work in this test.

Other schedulers can be used during testing, but that would require more complexity in the test code. For instance, if the default workflow scheduler were used, you would need a wait handle of some sort to block execution until the workflow completed. You can see an example of this type of waiting in Visual Studio by creating a new sequential workflow console application and examining the program.cs file.

The first test takes care of the normal execution, but what about exception cases? Most unit testing frameworks have the ability to declare expected exceptions to simplify test development. For example, this code is an example of how the unit testing in Visual Studio 2008 allows you to declare an expected exception on a test method:

[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void MailWithInvalidFrom(){...}

The problem with using this method when testing in Windows WF is that the runtime catches exceptions and, rather than throwing them in the host, raises an event when it terminates the workflow, passing the exception as part of the event arguments. This provides two options for handling exceptions: do not use the built-in behavior of the testing framework, or handle the terminated event and re-throw the exception.

You might think that throwing the exception from the event handler would work, but unfortunately it does not as the runtime catches the re-thrown exception. Figure 2 shows another test of the SendMailActivity, this time failing to pass a From address, which should cause an ArgumentNullException in the code.

Figure 2 Dealing with Exceptions

[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void MailWithInvalidFrom() {
  Exception ex = null;

  runtime.WorkflowTerminated += delegate(
    object sender, WorkflowTerminatedEventArgs wte) {
    ex = wte.Exception; };

  Dictionary<string, object> wfParams = 
    new Dictionary<string, object>();
  wfParams.Add("To", "mmnet30@microsoft.com");
  //wfParams.Add("From", "donotreply@example.org");
  wfParams.Add("Subject", "Unit testing exception");

  WorkflowInstance instance = runtime.CreateWorkflow(
  typeof(WFComponents.SendMailActivity), wfParams);

  instance.Start();

  ManualWorkflowSchedulerService man = 
    runtime.GetService<ManualWorkflowSchedulerService>();
  man.RunWorkflow(instance.InstanceId);

  //exception should be thrown by now
  if (ex != null)
  throw (ex);
  else
  Assert.Fail("Exception should have been thrown");
}

As you can see, the exception can be pulled from the arguments in the WorkflowTerminated event. But once that has happened, it is necessary to throw the exception in order to use the Expected­Exception behavior. At this point you might decide it is just as easy to use an Assert and make sure the exception is of the correct type rather than throwing the exception and using the Expected­Exception attribute.

Up to this point, you probably noticed that the testing code has not tested the actual output of sending the e-mail. In the case of e-mail, it is easy to configure the system.net/mailSettings configuration in the application configuration file to make sure messages are written to the file system. But then those messages must be opened and read to validate the output. A better way to test this functionality would be with a mock object that abstracts away the e-mail process.

In Windows WF, activities can either do work directly or use objects that are loaded into the runtime known as runtime services. One of the benefits of using runtime services to handle work is that the host process is in charge of adding the services into the runtime. If the activity is going to be used in an ASP.NET application, the code might be different than if the activity is being used in a Windows service. The activity simply queries the runtime for an object of a particular type then calls methods on it.

Using this model works very well for testing, as the runtime service can be replaced during tests with mock objects to help the testing effort. This might seem like a lot of extra work in your activities and a stiff requirement that all hosts must add a runtime service to support your activity. However, your activity can be written in such a way that if a runtime service is present, it uses it, and if not, the activity provides its own implementation.

Here is the execute method of the SendMailActivity showing how the execution can be different depending on whether a runtime service is present. If the service is found, the message is sent via the service; otherwise, the message is sent directly using the System.Net.Mail namespace:

IEmailService mailSvc =
  executionContext.GetService<IEmailService>();

MailMessage msg = new MailMessage(From, To);
msg.Subject = Subject;

//use the mail service if it is present
if (mailSvc != null) {
  mailSvc.SendMessage(msg);
}
else {
  SmtpClient client = new SmtpClient();
  client.Send(msg);
} 

Notice that the activity is using an interface instead of a concrete class, which provides a key abstraction point. The activity is not dependent on a specific implementation, rather it accepts the implementation of the service that is added by the runtime. The test then is able to use a mock implementation of the service to aid in testing as shown in Figure 3 .

Figure 3 Testing Activities with a Mock Runtime Service

[TestMethod]
public void TestEmailWithMock() {
  MockEmailService mock = new MockEmailService();
  runtime.AddService(mock);

  ...

  //the workflow is done, now we can test the outputs
  Assert.IsNotNull(results, "No results found");

  MockEmailService email = 
    runtime.GetService<MockEmailService>();
  Assert.IsNotNull(email, 
    "No email service found in the runtime");

  Assert.AreEqual<string>(wfParams["To"].ToString(),
    email.Message.To.ToString(), 
    "To address was changed");
}

Now the e-mail does not actually have to be sent as part of the test, and the outcome of the test is easier to validate using the implementation that was added. This method of abstraction works well for general hosting flexibility as well as testing.

Testing Rules

A critical component of many workflow solutions, business rules provide additional challenges to testing. Rulesets in Windows WF are not all that challenging to execute outside the context of a workflow, but a ruleset is defined based on a particular type, and that type is usually the workflow class. In general, rules need an object or set of objects as inputs that they can be evaluated against and take actions on. In the case of Windows WF, the workflow itself is usually the root object, and any other objects with which the rules interact are simply properties or activities in the workflow.

When you are executing workflows, the Policy activity is most often used to execute rules. In a unit test scenario, rules are better tested directly without an activity. To execute a ruleset, several classes from the System.Workflow.Activities.Rules namespace can be used. In addition, the RuleSet class can be deserialized from a stream using the WorkflowMarkupSerializer class so the representation of your rules that you use at runtime can be used in your test environment.

Figure 4 shows the basic code used to execute a ruleset in a unit test. Once the ruleset has finished executing, the objects used during the rule execution can be used in assertions to validate expected behavior. The important thing to notice first in this example is that the rules are being executed against a customer object instead of a workflow type. This is very intentional for several reasons.

Figure 4 Executing a Ruleset during a Test

//initialize test object
Customer cust = new Customer();
cust.Level = CustomerLevel.Gold;
cust.CurrentAnnualPurchases = 1578;

//create rule execution objects
RuleValidation val = 
  new RuleValidation(typeof(Customer), null);
RuleExecution exec = new RuleExecution(val, cust);
RuleSet ruleset = null;

//get rules from file / deserialize
WorkflowMarkupSerializer serializer = 
  new WorkflowMarkupSerializer();

string rulePath = Path.Combine(
  testContextInstance.TestDeploymentDir, "Customer.rules");
RuleDefinitions defs = 
  (RuleDefinitions)serializer.Deserialize(
  XmlReader.Create(rulePath));
ruleset = defs.RuleSets[0];

//execute ruleset
ruleset.Execute(exec);

//test the outcome
Assert.AreEqual<BLL.CustomerLevel>(cust.Level, 
  CustomerLevel.Platinum);

The default mode of creating rulesets using the Policy activity means that the policies are created based on the workflow type and are embedded as a resource in the assembly. This causes problems not just for testing—it can also be a challenge to flexibility at run time. For testing it provides a challenge, because in order to test the ruleset an instance of the workflow must be created and initialized. Creating rules against other types, such as business objects, makes testing much easier and can make using the rules from within your workflow easier. You can find several examples of working with rules, including creating custom activities, on the Windows WF MSDN site .

Testing the rules involves creating and initializing the object that acts as the input for the rules and initializing the ruleset and execution objects, which includes the rule validator and executor. The code shown is mostly boilerplate and can be used in custom activities that are built to execute rules on custom objects instead of workflows.

Sometimes this method of isolating the inputs for the rules into a separate type is not feasible, as the rules are dependent on properties of various activities in the workflow. In those cases, an instance of the workflow must be created using the CreateWorkflow method of the runtime and the properties of the activities initialized. Properties on the workflow can easily be initialized by passing parameters when creating it, but initializing the activities can be a bit more challenging. If the properties on the activities are dependency properties, then they can simply be bound to properties on the workflow and therefore initialized at time of creation.

If, however, the properties on the activities are standard .NET properties, a different approach is required. A simple approach is to put code in the Initialize method of the workflow and find all of the activity instances in the hierarchy and set their values. Note that you cannot do this work from the unit test itself because, once the workflow has been created, the host code does not have direct access to the running instance state.

The final wrinkle comes when using activities as inputs to your rules when the activities themselves are dynamically created, as in a While activity or a state machine. For more information on the issues related to iterating activities, see the June 2007 Foundations column, " ActivityExecutionContext in Workflows" . In these scenarios, the only way to have the activities correctly initialized for testing is to execute a workflow and have the composite activity create the activity clones. I am not going to spend time discussing this technique in detail because most developers shy away from writing rules in this scenario, as the rules themselves get too complex.

Testing Runtime Services

Runtime services refer to any .NET object added to the workflow runtime. There are services that ship with the Framework providing persistence, tracking, scheduling and other well-known services with which the runtime is familiar. These are services that are part of the framework and should not be the focus of your testing. However, when developing custom solutions, two kinds of runtime services might need testing: custom services that provide known functionality to the runtime and custom services that interact with activities or the runtime.

In the case of services that provide known functionality, such as a custom persistence service, the only real way to test these services is by executing them in the runtime. To manage the execution and control the test, the best method is to use a workflow as a driver. Using a workflow allows for controlling conditions, such as workflow idling, suspensions, and terminations, that might cause your service to be invoked.

When it comes to other services to be tested, the requirements for testing depend on the implementation of the service. For example, some services interact with the runtime via the events that it raises. Other services interact more directly with information about a workflow instance, such as requiring the instance identifier or using the current WorkBatch when called by an activity. In these scenarios, the service will be best tested in the context of a driver workflow. The methods shown at the beginning of this article can be used to drive the test.

Some objects only exist in the runtime to provide services to activities and do not rely on any other services. If this is the case, these services can generally be tested as standard .NET components. While there are no requirements that must be met from the perspective of the workflow runtime, the tests may still use mock objects for other services or dependent objects. Optionally, a single activity can be used as a driver, as shown earlier in this column, where the activity fully exercises the service ensuring that the tested functionality works when executed in the runtime. This last option might seem like extra work, but it also provides a bit more confidence that your code will execute correctly at run time.

Testing Workflows

One of the biggest challenges comes when developers attempt to unit test entire workflows. Because workflows often model long running processes that interact with many different services and applications, the complexity in testing quickly expands. For example, a given workflow may call several services using Windows Communication Foundation (WCF) and aggregate the results together as part of its logic.

It is possible to handle these interactions by injecting mock objects for testing. In fact, with the WCF activities in Windows WF, the ability to inject a testable service endpoint is simplified. The Send activity attempts to use the ChannelManagerService class to resolve endpoints. A test class can easily add named endpoints that match those expected in the workflow, changing the binding and address to point to a local service implementation. Using this indirection, a workflow that invokes several services can be tested in many cases. Please see the August 2008 issue of MSDN Magazine for more information on using the ChannelManagerService class .

In summary, it is not really a question of whether you need to test your workflow, rather it is a question of how you will test them. In many development teams, the testing of the workflow is not considered a unit testing task, rather it is an integration test that would take place after unit testing was complete and the various components are known to be in good working order. If you choose to use unit tests on your workflows or a driver to other automated testing, you can use the information presented in this article to help execute your tests.

Send your questions and comments to mmnet30@microsoft.com .

Matt Milner is an independent software consultant specializing in Microsoft technologies including .NET, Web services, Windows Workflow Foundation, Windows Communication Foundation, and BizTalk Server. As an instructor for Pluralsight, Matt teaches courses on Workflow, BizTalk Server, and Windows Communication Foundation. Matt lives in Minnesota with his wife, Kristen, and his two sons.