How to Unit Test a WorkflowService
Testing WorkflowServices is harder because you have to host the service somehow. There are several options you can use.
- You can host the service in IIS/AppFabric by configuring your project to use IIS
- You can try to insure that the local ASP.NET Development Server is started before testing (See my post on the Canonical REST Entity Service for an example).
- You can self-host the service
Unless your solution forces you to option 1 or 2 for some reason I recommend self-hosting for unit testing of services. The best solution is to use named pipes for your protocol because this avoids issues with URL ACLs for HTTP and Firewalls for TCP.
In this post I’m going to show you how I tested an example WorkflowService that illustrates how you can use the AddDefaultExtensionProvider method to insure that an extension exists when your activity needs it.
Watch endpoint.tv - How To Unit Test Workflows
Download Workflow Test Helper (including this sample code) from MSDN Code Gallery
Download WF4 WorkflowService Extension Example
For a simple example I created an activity that stores an integer and then returns a string that reports the number that was stored into the extension and the average of the collection. Here is a screenshot of the UI with the string returned from the activity.
When you have an activity that requires an extension you can provide a default mechanism for creating the extension if it does not already exist in the extensions collection by overriding Activity.CacheMetadata
public sealed class GetAverage : CodeActivity<string>
{
public InArgument<int> Number { get; set; }
protected override void CacheMetadata(CodeActivityMetadata metadata)
{
// This activity requires the average extension
metadata.RequireExtension(typeof(AverageExtension));
// This lambda will create one if it is not already added
metadata.AddDefaultExtensionProvider(() => new AverageExtension());
base.CacheMetadata(metadata);
}
protected override string Execute(CodeActivityContext context)
{
// Get the average extension
var average = context.GetExtension<AverageExtension>() as AverageExtension;
if (average == null)
throw new InvalidOperationException("Cannot access AverageExtension");
var number = Number.Get(context);
// Store this number
average.StoreNumber(number);
return string.Format("Stored {0}, Average:{1}", number, average.GetAverage());
}
}
Testing The Workflow Service
Once again, using WorkflowTestHelper makes it easy to test a WorkflowService. Step by step, here is how you do it.
- Add a reference to the binaries from WorkflowTestHelper to your test project
- Enable Deployment for VS Unit Tests
- Add the DeploymentItem attribute for the WorkflowService. The path is relative to the path of the solution (.sln) file
- Use the WorkflowServiceTestHost class to host the test
In the code below I’m using the ServiceClient proxy class from the web project to communicate with the workflow service
private readonly Binding _binding = new NetNamedPipeBinding();
/// <summary>
/// The endpoint address to be used by the test host
/// </summary>
private readonly EndpointAddress _serviceAddress = new EndpointAddress("net.pipe://localhost/TestService");
/// <summary>
/// Tests the GetAverage Activity by invoking it in a WorkflowService multiple times
/// </summary>
[TestMethod()]
[DeploymentItem("Service1.xamlx")]
public void ShouldInvokeAverageExtension()
{
const string expected1 = "Stored 33, Average:33";
const string expected2 = "Stored 44, Average:38.5";
const string expected3 = "Stored 55, Average:44";
string result1;
string result2;
string result3;
// Self-Host Service1.xamlx using Named Pipes
using (var testHost = WorkflowServiceTestHost.Open("Service1.xamlx", _serviceAddress.Uri))
{
// Use the generated proxy with named pipes
var proxy = new ServiceClient(_binding, _serviceAddress);
try
{
result1 = proxy.GetData(33);
result2 = proxy.GetData(44);
result3 = proxy.GetData(55);
proxy.Close();
}
catch (Exception)
{
proxy.Abort();
throw;
}
Assert.AreEqual(expected1, result1);
Assert.AreEqual(expected2, result2);
Assert.AreEqual(expected3, result3);
}
}
So Get Busy Testing
I’m always surprised by how many people do little or no testing of their code. You can probably tell that I’m a big fan of testing and going even further I’m a proponent of Test Driven Development (or Test-First if you prefer). When I started working with WF4 I realized how difficult this can be. We still have a way to go to make it even better and I’ve got some more surprises in store for you so stay tuned…
Comments
- Anonymous
September 13, 2010
I've found that I don't test the workflow service xamlx, I actually test activities that use Receive and Send activities in their implementation. To do this I had created a WorkflowServiceInvoker much like your WorkflowServiceTestHost. Some useful features I added were:
- An action that could be performed once the service had idled
- A Predicate based on tracking data for optionally specifying a condition to wait for.
- A function to provide me a proxy to a service exposed by Receive endpoints on the WSH given an interface that matches the service description. Other testing helpers I have created (also on my blog) are:
- A TestContext tracking participant, useful for getting tracking records for failed tests
- A Dependency Injection workflow extension which allows substituting dependencies when you don't control the construction of your activities (e.g. when testing a composite / XAML activity.
- Anonymous
September 13, 2010
Nice work Peter - I like your ideas - thanks for sharing them!