TDD and Windows Workflow Foundation

In the past year I learned a lot about unit testing and TDD in general.  After meeting some very passionate TDD type guys like Peter Provost (see this ARCast.TV interview for more) and Roy Osherove I became convinced that this style of development offers significant benefits.  Of course most things you read about TDD don't really discuss unit testing in an environment like Windows Workflow Foundation (WF).  Recently I've been asked how you can unit test workflows so today lets consider a simple case of testing a workflow.

Our Test Workflow

To make this very simple I'm going to use a workflow that does nothing more than start and complete.  I simply pass a value into it and get a value out of it.  

For illustration purposes let's assume that

  • The input is an Int32 that must be between 1 and 10.
  • The output will be the input * 2

If you want to following along at home try this...

Step 1 - Create the workflow library and workflow

  1. In VS2008 select File / New Project
  2. Select the Workflow template Sequential Workflow Library, name the library WorkflowLibrary1. 
    You should now have an empty workflow

Step 2 - Create the unit test

  1. Right click on the workflow and select View Code
  2. Right click on the class name and select Create Unit Tests (not all versions of VS include this feature) be patient VS will build the solution and then display a dialog with the members it will test
  3. Uncheck all the test methods except for the Workflow1() constructor and click Create unit tests
  4. Name the test project TestProject1 (default name)

Step 3 - Think about the tests and the code

As Peter Provost said in this interview, the first thing we should do is ask ourselves a question.  How would I know if this bit of code I'm about to write does the right thing?  As we think about it, a bunch of assertions pop out.

  1. The workflow would accept a number n and return n*2 as an output
  2. The workflow would throw an ArgumentOutOfRangeException if the number n was less than 1 or greater than 10

To test these assertions I'm going to create a number of scenarios using the Behavioral Driven Development template for scenarios

Scenario: Happy Path

Given that the Input value of 2
Ensure that the workflow returns an Output value of 4

Scenario: Invalid Lower Bound

Given that the Input value is 0
Ensure that the workflow terminates with an ArgumentOutOfRangeException

Scenario: Invalid Upper Bound

Given that the Input value is 11
Ensure that the workflow terminates with an ArgumentOutOfRangeException

Step 4 - Write the test first

Now I'm going to create my first test.  At this point I do have a workflow that does nothing so the test will fail as it should.  There are some interesting elements to this unit test that are commented in the code so pay careful attention

 [TestMethod()]
public void ShouldReturn4WhenInputIs2()
{
    int expected = 4;
    int input = 2;
    int output = 0;

    // Create a runtime
    WorkflowRuntime runtime = new WorkflowRuntime();

    // Use the manual workflow scheduler service to make the workflow execute
    // on the test thread synchronously
    ManualWorkflowSchedulerService manualScheduler = new ManualWorkflowSchedulerService();

    // Add the scheduler to the runtime before it is started
    runtime.AddService(manualScheduler);


    // Handle the workflow completed event so you can capture the output
    runtime.WorkflowCompleted += (o, e) =>
        {
            // Note you cannot Assert the value here
            // because failures are thrown exceptions which will
            // be caught by the caller - the workflowRuntime and
            // will not propagate to the test framework
            output = (int)e.OutputParameters["Output"];
        };

    // Setup the input parameters
    Dictionary args = new Dictionary();

    // The name of the argument here must match the name of the property
    // on the workflow class
    args.Add("Input", input);

    // Create the workflow passing the arguments
    WorkflowInstance targetWorkflow = runtime.CreateWorkflow(typeof(Workflow1), args);

    // Start the workflow (note: it won't run until the scheduler runs it)
    targetWorkflow.Start();

    // Run the workflow
    manualScheduler.RunWorkflow(targetWorkflow.InstanceId);

    // Assert the results
    Assert.AreEqual(expected, output);
}

Step 5 - Run the test and see it fail

Now you can run the unit test (Shortcut Ctrl+R,A) and it will fail with an exception

 Failed    ShouldReturn4WhenInputIs2    TestProject1    Test method TestProject1.Workflow1Test.ShouldReturn4WhenInputIs2 
threw exception:  System.ArgumentException: The activity 'Workflow1' has no public writable property named 'Input'; 

This is good - it should fail because we haven't implemented the code yet.  However we did make 2 decisions about the interface to this workflow.  We decided that there are two public properties named Input and Output that will be used to get data in and out of the workflow

Step 6 - Write just enough code to make the test pass

Now we want our test to turn green - to pass.  I don't want to write more code than is necessary so here is the plan

  • Add the Input and Output properties that make up the public interface to this workflow
  • Add the activity that will do the arithmetic to multiply our input times 2.

Note: I am not validating arguments yet because this test does not require this.

  1. In the workflow designer drag a code activity onto the design surface and double click it to add an execution handler.
  2. Add the properties and code to multiply.  The workflow should have the following code in it
 public sealed partial class Workflow1 : SequentialWorkflowActivity
{
    public Workflow1()
    {
        InitializeComponent();
    }

    public int Input { get; set; }
    public int Output { get; set; }

    private void codeActivity1_ExecuteCode(object sender, EventArgs e)
    {
        Output = Input * 2;
    }
}

Step 7 - Run unit tests and see them pass

Now I can run my unit test again (Ctrl+R,A) and I see that my test is passing. 

Step 8 - Refactor

The old saying goes... "Red, Green, Refactor".  At this point I should take a hard look at the code and ask if it makes sense to refactor either the test code or the application code.  After a quick glance I would say that there isn't much refactoring to be done here so I'll continue on.

Step 9 - Write tests for remaining assertions

I said that my code show through ArgumentOutOfRangeException for Input values outside of the 1-10 range.  Handling exceptions from workflows is different than handling standard exceptions.  For example, you can't use the [ExpectedException(...)] attribute to handle these exceptions without doing some fancy footwork as shown below, because when the workflow throws an exception the WorkflowRuntime will catch it, invoke any attached FaultHandlerActivity(s) and then it will terminate the WorkflowInstance.  The test code will have to handle the WorkflowTerminated event to know about this happening as shown in this test code.

 [TestMethod()]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ShouldThrowArgumentOutOfRangeWhenInputIs0()
{
    ArgumentOutOfRangeException workflowException = null;

    // A value of 0 should result in an ArgumentOutOfRangeException
    int input = 0;

    // Create a runtime
    WorkflowRuntime runtime = new WorkflowRuntime();

    // Use the manual workflow scheduler service to make the workflow execute
    // on the test thread synchronously
    ManualWorkflowSchedulerService manualScheduler = new ManualWorkflowSchedulerService();

    // Add the scheduler to the runtime before it is started
    runtime.AddService(manualScheduler);

    // Handle the WorkflowTerminated event so you can see what the exception and message were
    runtime.WorkflowTerminated += (o, e) =>
        {
            // Note: You cannot Assert anything here
            // because failures are thrown as exceptions
            // save the value and assert later

            // We are expecting this kind of exception - if it is 
            // anything else it will result in a null
            workflowException = e.Exception as ArgumentOutOfRangeException;
        };

    // Setup the input parameters
    Dictionary args = new Dictionary();

    // The name of the argument here must match the name of the property
    // on the workflow class
    args.Add("Input", input);

    // Create the workflow passing the arguments
    WorkflowInstance targetWorkflow = runtime.CreateWorkflow(typeof(Workflow1), args);

    // Start the workflow (note: it won't run until the scheduler runs it)
    targetWorkflow.Start();

    // Run the workflow
    manualScheduler.RunWorkflow(targetWorkflow.InstanceId);

     // Make sure we got the right exception
    Assert.IsNotNull(workflowException, "No ArgumentOutOfRangeException received from workflow");

    // Re throw the exception
    throw workflowException;
}

Step 10 - Run the test and see it fail

Run the unit tests again (Ctrl+R,A) and notice that now one test is passing and the other is failing with the following exception

 Failed    ShouldThrowArgumentOutOfRangeWhenInputIs0   TestProject1    Assert.IsFalse failed. 
Workflow completed when it should have terminated because of an Input of 0   

Step 11 - Write just enough code to make the validation test pass

Now I need to add validation of the Input argument.  To do that I've modified the code to throw an exception for an invalid value on the low end of the range

 private void codeActivity1_ExecuteCode(object sender, EventArgs e)
{
    if (Input < 1) throw new ArgumentOutOfRangeException("Input");
    Output = Input * 2;
}

Step 12 - Refactor

Now we have some duplication of code between tests.  Both of them create a runtime and manual scheduler.  I could refactor the code to create a helper method that would create the runtime and add the manual scheduler but it won't save many lines of code and would probably just make the test code a little more complex.  On the other hand I need to create another validation test that will be exactly like the first one but with a different value.  This is a great opportunity to refactor by extracting the body of that test into a method that will test to see that an invalid input arg (of whatever value) results in an ArgumentOutOfRangeException

Step 13 - Write test to validate Input not greater than 10

Since the refactoring took place, this is now a simple 1 line of code to invoke the extracted method with a different parameter.  My two test methods now look like this.

 [TestMethod()]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ShouldThrowArgumentOutOfRangeWhenInputIs0()
{
    ShouldThrowExceptionOnInvalidValue(0);
}

[TestMethod()]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ShouldThrowArgumentOutOfRangeWhenInputIs11()
{
    ShouldThrowExceptionOnInvalidValue(11);
}

Step 14 - Run the test and see it fail

Now I run my test and see it fail for an input of 11.  All that remains now is to validate the upper bound of the argument and throw an argument exception

Step 15 - Modify the code to validate the upper bound of the argument

You might ask - why didn't I do this earlier when I modified the code to check the lower bound?  I suppose it is just a habit, a discipline if you will to not do anything until a test requires it.  This habit insures that nothing in my code is untested.  So once again I modify my code to handle the upper bound validation

 private void codeActivity1_ExecuteCode(object sender, EventArgs e)
{
    if (Input < 1) throw new ArgumentOutOfRangeException("Input");
    if (Input > 10) throw new ArgumentOutOfRangeException("Input");
    Output = Input * 2;
}

Step 15 - Run the test and see it pass

Run the unit tests again (Ctrl+R,A) - now all tests are passing.

Step 16 - Pause and reflect on what you have accomplished

If this seems like a lot of work... well, let's just admit that it is.  Could you have written a simple workflow like this in less time?  Why bother with all this testing?

I bother with it because I, like you, have experienced too many applications that just didn't  work.  Most people verify that their application worked at some point in the past.  But as things change around it, platform, server, surrounding code, config etc.  things can break.

Having these three tests for my simple workflow will give me a way to know for certain that the code is verifiably correct in the future.  And that is worth the effort.

Next Time... Lessons learned from this little exercise

When I started on this blog post this morning I was surprised by several things along the way.  Next time I'll share with you the lessons learned so these little issues don't surprise you.

Sample code for this test is here