TDD with Microsoft.Activities.UnitTesting

Microsoft.Activities.UnitTesting is a unit testing framework designed to make it easier to unit test activities and workflow services built with Windows Workflow Foundation (WF4).
The Framework contains
  • Test Hosts which wrap WorkflowInvoker, WorkflowApplication and WorkflowServiceHost
  • Assert helper classes for asserting out arguments and tracking records
These classes are test framework independent so you can use them with MSTest, NUnit, xUnit or whatever test framework you like. In the walkthroughs we will use MSTest

Walkthrough – Hello WF4 Unit Testing

Download this walkthrough in Word Format TDD With Microsoft.Activities.UnitTesting.docx

In this walkthrough you will learn the basics of unit testing a simple workflow with Microsoft.Activities.UnitTesting. Our task is to build a workflow which meets the following requirements
Given When Then
Two positive integers The workflow is invoked with the integers passed as arguments Num1 and Num2 The sum is returned in an out argument named Sum
One positive and one negative integer The workflow is invoked with the integers passed as arguments Num1 and Num2 An ArgumentOutOfRangeException is thrown

Task 1 – Create the Application and Test projects

  • Start Visual Studio and create a new Workflow Console Application named HelloWFTest
  • Right click on the solution and select Add New Project
  • Add a new Test Project named HelloWFTest.Tests
  • Add references to HelloWFTest and System.Activities
  • Rename UnitTest1 to Workflow1Tests
  • Right click on HelloWFTest.Tests and select Add / New Folder
  • Name the new folder Reference Assemblies
  • Download and extract Microsoft.Activities.UnitTesting to the Reference Assemblies folder
  • Add references to Microsoft.Activities.dll and Microsoft.Activities.UnitTesting.dll

Task 2 – Write the First Test

When you write a test the question you want to ask yourself is "How would I know if this workflow is working correctly?" In our case we can simply verify that an argument named sum is returned with expected values.
  • In the HelloWFTests.Tests project open Workflow1Tests.cs
  • Rename TestMethod1 to Workflow1ShouldReturnSum
  • Because our workflow is simple and will not use any bookmarks we will use WorkflowInvokerTest to test our activity. Add the following code to your test method.
/// <summary>
/// Given
/// * Two positive integers
/// When 
/// * The workflow is invoked with the integers passed as Num1 and Num2
/// Then
/// * The sum is returned in an out argument named Sum
/// </summary>
[TestMethod]
public void Workflow1ShouldReturnSum()
{
    const int Expected = 4;

    // Create the activity
    var activity = new Workflow1();

    // Create the test host
    var host = new WorkflowInvokerTest(activity);

    try
    {
        // Create our input arguments using a helper class
        var input = InputDictionary.Create("Num1", 3, "Num2", 1);

        // Will invoke the activity and capture the results
        host.TestActivity(input);

        // Verify the sum argument
        host.AssertOutArgument.AreEqual("Sum", Expected);
    }
    finally
    {
        // Dump the tracking information to the test log
        host.Tracking.Trace();
    }
}

Why use try/finally in the test?

All of the test host classes capture tracking information from the workflow as it executes. If your test fails an exception will be thrown. In the finally block we will dump the tracking information to the test log. This will help you to diagnose why the test is failing.

Task 3 – Run the test and see it fail

It is important to see your test fail. You may accidentally write a test that always passes.
  • Run the test. At this point you don't need to debug it. You can quickly run the unit test without debugging by pressing Ctrl+R, T or select Test / Run / Tests in Current Context from the menu.
The test will fail because we have not implemented the workflow

Task 4 – Add the Input Arguments

As you make the test pass, you should do only what the test requires you to do. When we examine the results of the first test run we see an exception was thrown because of the input arguments. In this task we will fix this problem. Of course we know that there will be other problems that we have to fix later. However by fixing just this problem we ensure that our workflow only does what the test demands of it and nothing more.
If we find that the workflow doesn't do something it needs to do then we need to make the tests demand more. We keep repeating this process of running the test and fixing issues until the test passes. If we follow this process carefully at the end we will have a workflow that does only what the test asks of it and a test that demonstrates that the workflow does exactly what it is supposed to do and nothing more.
  • Open Workflow1.xaml
  • Open the Arguments window and add two arguments
Name Direction Argument type
Num1 In Int32
Num2 In Int32
  • Run the unit test again, it will now fail because there is no out argument named Sum.
  • Double click on the failed test. Notice how the Debug Trace area provides a dump of the tracking information
*** Tracking data follows ***
WorkflowInstance <Workflow1> is <Started> at 10:38:55.8250
Activity <null> is scheduled child activity <Workflow1> at 10:38:55.8250
Activity <Workflow1> state is Executing at 10:38:55.8250
{
    Arguments
        Num1: 3
        Num2: 1
}
Activity <Workflow1> state is Closed at 10:38:55.8250
{
    Arguments
        Num1: 3
        Num2: 1
}
WorkflowInstance <Workflow1> is <Completed> at 10:38:55.8406

Task 5 – Add an Out Argument named Sum

  • Add another argument
Name Direction Argument type
Sum Out Int32
  • Run the unit test again. This time the test fails because the Sum argument has a value of zero.

Task 6 – Assign a value to Sum

At this point you are probably thinking it is about time that we implemented the workflow. After all we know that we want to assign Sum = Num1+Num2 don't we? Actually we think we want that but it is a good exercise to avoid doing anything more than what the tests actually require. After all the test thus far requires only that we return Sum = 4 so let's implement just that.
  • Open Workflow1.xaml
  • Drop an Assign activity and set the properties
Property Value
To Sum
Value 4
  • Run the unit test again. It passes

Task 7 – Refactor

"Red, Green, Refactor" is the plan. So far we have seen the test fail (Red) then pass (Green) and now it is time to refactor. At this stage we look at our code and ask ourselves could we make it better? If we want to change it we can do so more confidently because we have a test to verify the behavior.
For our purposes we know that our test is passing but we also know we are not done. The scenario indicates that for any two positive integers we should be able to get the sum. Right now our workflow only functions for any two integers where the sum is 4. This means we need to write another test which simply passes different numbers to our workflow. Because everything else in the test is going to be the same this is a good opportunity to refactor.
  • Extract the body of the test method into a new method.
private static void Workflow1ShouldReturnSumOfValues(int num1, int num2)
{
    var expected = num1 + num2;

    // Create the activity
    var activity = new Workflow1();

    // Create the test host
    var host = new WorkflowInvokerTest(activity);

    try
    {
        // Create our input arguments using a helper class
        var input = InputDictionary.Create("Num1", num1, "Num2", num2);

        // Will invoke the activity and capture the results
        host.TestActivity(input);

        // Verify the sum argument
        host.AssertOutArgument.AreEqual("Sum", expected);
    }
    finally
    {
        // Dump the tracking information to the test log
        host.Tracking.Trace();
    }
}
  • Modify your existing test and description
/// <summary>
/// Given
///   * Two positive integers 3 and 1
///   When 
///   * The workflow is invoked with the integers passed as Num1 and Num2
///   Then
///   * The sum is returned in an out argument named Sum
/// </summary>
[TestMethod]
public void Workflow1ShouldReturnSumOf3And1()
{
    Workflow1ShouldReturnSumOfValues(3, 1);
}
  • Create a new test that verifies different values
/// <summary>
/// Given
///   * Two positive integers 0 and int.MaxValue
///   When 
///   * The workflow is invoked with the integers passed as Num1 and Num2
///   Then
///   * The sum is returned in an out argument named Sum
/// </summary>
[TestMethod]
public void Workflow1ShouldReturnSumOf0AndMaxInt()
{
    Workflow1ShouldReturnSumOfValues(0, int.MaxValue);
}
  • Run both tests, now one will pass and one will fail.

Does this seem like a lot of work?

Right now it might be tempting to think about skipping many of these steps because you are already quite familiar with this project. But a year from now when you or another team member look at this code you will be very glad you took the time to build these tests.

Task 8 – Make the test pass

  • Open Workflow1.xaml
  • Modify the assign activity properties
Property Value
To Sum
Value Num1 + Num2
  • Run the tests again, they will pass

Task 9 – Test the second scenario

So far we have good test coverage of the first scenario but we do not have test coverage for the second. In the second scenario we need to test what happens when our workflow has an unhandled exception. We want to verify that it throws the correct exception type.
Different unit test frameworks provide varying methods for testing exceptions. Microsoft.Activities.UnitTesting includes a class called AssertHelper to support exception testing which we will use in this example.
Given When Then
One positive and one negative integer The workflow is invoked with the integers passed as arguments Num1 and Num2 An ArgumentOutOfRangeException is thrown
  • Create another test method to verify this scenario
/// <summary>
/// Given
///   * One positive integers 1 and one negative integer -1
///   When 
///   * The workflow is invoked with the integers passed as Num1 and Num2
///   Then
///   * An ArgumentOutOfRangeException is thrown
/// </summary>
[TestMethod]
public void Workflow1ShouldThrowArgOutOfRangeWhenNegativeArgPassed()
{
    AssertHelper.Throws<ArgumentOutOfRangeException>(
      () => Workflow1ShouldReturnSumOfValues(1, -1));
}
  • Run the test. It will fail because our workflow does not throw an exception yet.

Task 10 – Make the test pass

  • Open Workflow1.xaml
  • Right click on the Assign activity and select Cut
  • Drop an If activity
  • Inside the Then area right click and select Paste
  • Set the condition of the expression (Note: this is incorrect but we want to simulate a bug)
Num1 < 0 Or Num2 < 0
  • Drop a Throw activity on the Else side
  • Set the Exception property
New ArgumentOutOfRangeException()
  • Run the test, it will fail

Task 11 – Diagnose a Test Failure with Tracking

Often when a workflow is failing it can be difficult to know why. In this case the bug is in a Visual Basic expression in the If condition. Microsoft.Activities.UnitTesting makes it easier to see what is going on inside of your activity as it executed. Our test is failing because the exception was not thrown. In our simple activity we know what the problem is but in more complex activities it might be quite difficult to find out what happened.
  • Double click the Test Results and look at the tracking information.
  • Find where the <If> activity is executing. Notice the arguments. The condition evaluated to True when it should have been false. Now we know where the error is.
Tracking Data
Activity <If> state is Executing at 11:45:53.2127
{
    Arguments
        Condition: True
}
  • Correct the Condition expression
Num1 >= 0 AndAlso Num2 >= 0
  • Run the unit tests again, now all are passing

Why run all the tests again?

As we changed the workflow we may have broken a scenario that was passing earlier. Unit tests will help you to detect regressions early on and fix them quickly.

Last edited Feb 10, 2011 at 11:25 PM by ronjacobs, version 3

Comments

Ahf0124 Mar 21, 2012 at 9:53 AM 
Thank you for your great content.

I translate Japanese that you have created, a document of unit tests ( TDD With Microsoft.Activities.UnitTesting.docx).
If there is no problem if, I think trying to publish in my Blog.

Is this no problem?