Freigeben über


WF4: How to Unit Test a Workflow that calls a WCF Service

“The important point is that our test can’t control what that dependency returns to our code under test or how it behaves (if we wanted to simulate an exception, for example). That’s when we use stubs.” – The Art of Unit Testing - Roy Osherove,

Yesterday morning I received an email from Ryo in Belgium who asked for help in unit testing a workflow.  Ryo had read my previous posts on Microsoft.Activities.UnitTesting.XamlInjector and wanted to use it to test his workflow but was having trouble.  I gave him a quick suggestion and a little later he wrote back still having trouble and sent me some test code so I decided to fix it for him.  Several hours later I had a new update to Microsoft.Activities.UnitTesting which I will share with you now.

Note: This video uses smooth streaming. If it is hard to read the screen, just go full screen and wait about 30 seconds for the smooth streaming to kick in

Scenario: Unit Testing a Workflow that calls a WCF service

Given
  • A console application with a self-hosted WCF Calculator Service
  • A WF4 Workflow that
    • Accepts arguments to pass to the WCF service
    • and uses Send and ReceiveReply activities to invoke the WCF Service
    • and returns the result to the caller
  • A Unit Test that runs the workflow and verifies that the workflow returns correct output when provided correct input
When
  • The unit test is run
  • and the console application is not running
Then
  • The unit test should complete successfully

 

What Are We Testing Exactly?

In this scenario there are two components.

  1. The WCF Calculator Service
  2. The Workflow which has a dependency on the service

As Roy Osherove said in the quote at the top of this post we want to avoid external dependencies in our unit testing.  We should test both components in isolation such that one does not depend on the other at test time.

Testing Workflows With Stubs

Here is the unit test that Ryo provided

 //Not good, need the Wcf service running
 [TestMethod]
 public void TestMethod3()
 {
     IDictionary<string, object> result = WorkflowInvoker.Invoke(new Workflow1()
     {
         x = 3,
         y = 5
     });
  
     Assert.AreEqual(8, result["result"]);
     Assert.AreNotEqual(10, result["result"]);            
 }     

To eliminate the WCF Service from our unit test we will use stubs.  In our workflow there are two activities that need to be replaced with a stub.

Activity Behavior Stub Behavior
Send Sends a message to the service. If the service cannot be reached the activity will throw an exception Do nothing, assume that the message will be sent succesfully.
ReceiveReply Receives a response from the service and stores the value in variables or arguments in the workflow Store an expected response in the variables or arguments of the workflow

When you create a stub you create an object that has the same method signature.  Activities in Workflow also have a signature.  The way to see it clearly is to look at the XAML.  Here is the Send activity. 

 <p1:Send x:Name="__ReferenceID0" OperationName="Sum" ServiceContractName="p:ICalc">
   <p1:Send.CorrelationInitializers>
     <p1:RequestReplyCorrelationInitializer CorrelationHandle="[__handle1]" />
   </p1:Send.CorrelationInitializers>
   <p1:Send.Endpoint>
     <p1:Endpoint AddressUri="net.tcp://localhost:8009/CalculatorService/Calculator">
       <p1:Endpoint.Binding>
         <p1:NetTcpBinding Name="netTcpBinding" />
       </p1:Endpoint.Binding>
     </p1:Endpoint>
   </p1:Send.Endpoint>
   <p1:SendParametersContent>
     <InArgument x:TypeArguments="x:Int32" x:Key="a">[x]</InArgument>
     <InArgument x:TypeArguments="x:Int32" x:Key="b">[y]</InArgument>
   </p1:SendParametersContent>
 </p1:Send>

You can see that it has a number of properties that need to be duplicated in the stub send activity.  The goal is to rewrite the XAML so that all we do is replace Send with SendStub.  Everything else should remain the same.  Sounds simple right?  I thought so too until I tried to create one.  The good news is that after several hours, I came up with a set of messaging stub activities that are now in the latest release of Microsoft.Activities.UnitTesting so you won’t have to build them.  Using these stubs I was able to create a unit test that implemented our scenario perfectly.

 using Microsoft.Activities.UnitTesting.Stubs;
  
 [TestClass]
 public class UnitTest1
 {
     /// <summary>
     /// Invokes the Workflow but does not actually send or receive messages. 
     /// </summary>
     /// <remarks>
     /// This type of test can validate the logic of the workflow without sending/receiving messages.
     /// It does this by replacing the messaging activities with stubs as it creates a 
     /// test version of the XAML and runs it.
     /// Note: You must deploy the .xaml file for this test to work
     /// </remarks>
     [TestMethod]
     [DeploymentItem(@"MyWorkflow\Workflow1.xaml")]
     public void TestMethod1()
     {
         const int expectedResult = 12;
  
         // The XamlInjector will create our injected xaml file Workflow1.Test.xaml 
         // in the test output directory
         var xamlInjector = new XamlInjector("Workflow1.xaml");
  
         // If you need to simulate sending the actual values you can do this
         // Create send parameters using the names on the send activity
         var sendParameters = new Dictionary<string, object>() { { "a", 5 }, { "b", 7 } };
         var sendImplementation = new SendParametersStubImplementation(sendParameters);
  
         // Replace the sender with the SendStub
         xamlInjector.ReplaceAll(typeof(Send), typeof(SendStub));
  
         // Setup the response parameters using the name on the ReceiveReply activity
         var receiveParameters = new Dictionary<string, object>() { { "SumResult", expectedResult } };
         var receiveImplementation = new ReceiveParametersStubImplementation(receiveParameters);
  
         // You must replace the receiver or the workflow will wait for a response
         xamlInjector.ReplaceAll(typeof(ReceiveReply), typeof(ReceiveReplyStub));
  
         // Have to create an invoker so we can add extensions
         WorkflowInvokerTest host = new WorkflowInvokerTest(xamlInjector.GetActivity());
  
         // Stubs look for extensions to provide implementation
         host.Invoker.Extensions.Add(sendImplementation);
         host.Invoker.Extensions.Add(receiveImplementation);
  
         // Stubs eliminate bookmarks so WorkflowInvoker can test the activity
         // Messaging activities cannot be tested with invoker otherwise.
         var result = host.TestActivity();
  
         // This assert is ok
         Assert.AreEqual(expectedResult, result["result"]);
  
         // This is better - it provides better error messages and checks
         host.AssertOutArgument.AreEqual("result", expectedResult);
  
         // To see what is going on, trace the tracking info and check the test results
         host.Tracking.Trace();
     }

 

Happy Coding!

Ron Jacobs
https://blogs.msdn.com/rjacobs
Twitter: @ronljacobs

TestSolution.zip

Comments

  • Anonymous
    January 20, 2011
    hi, cool post. Is there a way to get the sample code.

  • Anonymous
    January 20, 2011
    Sample code now attached

  • Anonymous
    January 29, 2011
    thanks for the explanations

  • Anonymous
    February 16, 2011
    Hi Ron, Firstly, wonderful insight on the UT of the workflows. Thanks a lot. I come across this strange warning. It's not show stopper for us at the moment as I changed .net targeted framework to .net 4.0 from .net 4.0 Client profile. But was wondering if you could shed some light on this? Warning 42 The referenced assembly "Microsoft.Activities.UnitTesting" could not be resolved because it has a dependency on "System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" which is not in the currently targeted framework ".NETFramework,Version=v4.0,Profile=Client". Please remove references to assemblies not in the targeted framework or consider retargeting your project. Tribold.PPM.Workflow.ActiviteLibrary.UnitTest

  • Anonymous
    February 17, 2011
    The MSTest assemblies require .NET 4 profile that is why Microsoft.Activities.UnitTesting requires .NET 4 profile.