This morning I’ve been working on how to support cancelling a workflow via a CancellationToken.  The details of that are not important right now but what is really cool is how I was able to test this.

Scenario: Caller requests Cancellation via a CancellationToken and the UnhandledExceptionAction is Cancel

Given

  • An activity that contains a CancellationScope
  • The CancellationScope body has an activity that will create a bookmark and go idle
  • The CancellationScope has a CancelHandler with a WriteLine that has a DisplayName "CancelHandlerWriteLine"

When

  • The caller invokes the workflow asynchronously as a task with a CancellationToken
  • and in the idle callback calls CancellationTokenSource.Cancel

Then

  • A TaskCanceledException is thrown
  • The WorkflowApplication is canceled
  • The CancelationScope CancelHandler is invoked
Test Challenges
  • How can I wait until the cancel is completed after handling the exception before verifying?
  • How will I verify that the CancelHandler is invoked?
Solution

To wait until the cancel is completed after handling the exception before verifying I simply create an AutoResetEvent (line 18) and signal it from the WorkflowApplication.Completed event callback (line 19).  Then before verifying the tracking data I wait for this event (line 41)

To verify that the cancel handler was invoked I use the Microsoft.Activities.UnitTesting.Tracking.MemoryTrackingParticipant. This allows me to capture the tracking information into a collection that I can search using AssertTracking.Exists to verify that the activity with the name ExpectedCancelWriteline entered the Closed state.

   1: [TestMethod]

   2: public void ActivityIsCanceledViaTokenShouldInvokeCancelHandler()

   3: {

   4:     const string ExpectedCancelWriteLine = "CancelHandlerWriteLine";

   5:     var workflowApplication =

   6:         new WorkflowApplication(

   7:             new CancellationScope

   8:                 {

   9:                     Body = new TestBookmark<int> { BookmarkName = "TestBookmark" }, 

  10:                     CancellationHandler = new WriteLine { DisplayName = ExpectedCancelWriteLine }

  11:                 });

  12:  

  13:     // Capture tracking events in memory

  14:     var trackingParticipant = new MemoryTrackingParticipant();

  15:     workflowApplication.Extensions.Add(trackingParticipant);

  16:  

  17:     // Use this event to wait until the cancel is completed

  18:     var completedEvent = new AutoResetEvent(false);

  19:     workflowApplication.Completed = args => completedEvent.Set();

  20:  

  21:     try

  22:     {

  23:         var tokenSource = new CancellationTokenSource();

  24:  

  25:         // Run the activity and cancel in the idle callback

  26:         var task = workflowApplication.RunEpisodeAsync(

  27:             (args, bn) =>

  28:                 {

  29:                     Debug.WriteLine("Idle callback - cancel");

  30:                     tokenSource.Cancel();

  31:                     return false;

  32:                 }, 

  33:             UnhandledExceptionAction.Cancel, 

  34:             TimeSpan.FromMilliseconds(1000), 

  35:             tokenSource.Token);

  36:  

  37:         // Exception is thrown when Wait() or Result is accessed

  38:         AssertHelper.Throws<TaskCanceledException>(task);

  39:  

  40:         // Wait for the workflow to complete the cancel

  41:         completedEvent.WaitOne(this.DefaultTimeout);

  42:  

  43:         // Verify the the cancel handler was invoked

  44:         AssertTracking.Exists(

  45:             trackingParticipant.Records, ExpectedCancelWriteLine, ActivityInstanceState.Closed);

  46:     }

  47:     finally

  48:     {

  49:         // Write the tracking records to the test output

  50:         trackingParticipant.Trace();

  51:     }

  52: }

  53:  

When I run this test I also get the Tracking info in the Test Results along with any Debug.WriteLine output to help me sort out what is happening.  The tracking data is nicely formatted thanks to extension methods in Microsoft.Activities.UnitTesting.Tracking that provide a Trace method for each type of tracking record which produces human readable formatting.

WaitForWorkflow waiting for workflowBusy - check for cancel

Checking cancel token

System.Activities.WorkflowApplicationIdleEventArgs

    Bookmarks count 1 (TestBookmark)

Idle callback - cancel

Checking cancel token from idle handler

Cancel requested canceling workflow 

WaitForWorkflow workflowBusy is signaled - check for cancel

Checking cancel token

Cancel requested canceling workflow 

WorkflowApplication.Cancel

this.CancellationToken.ThrowIfCancellationRequested()

*** Tracking data follows ***

WorkflowInstance for Activity <CancellationScope> state is <Started> at 04:13:53.7852

Activity <null> is scheduled child activity <CancellationScope> at 04:13:53.7852

Activity <CancellationScope> state is Executing at 04:13:53.7852

Activity <CancellationScope> is scheduled child activity <TestBookmark> at 04:13:53.7852

Activity <TestBookmark> state is Executing at 04:13:53.7852

{

    Arguments

        BookmarkName: TestBookmark

}

WorkflowInstance for Activity <CancellationScope> state is <Idle> at 04:13:53.7852

Activity <null> cancel is requested for child activity <CancellationScope> at 04:13:53.7852

Activity <CancellationScope> cancel is requested for child activity <TestBookmark> at 04:13:53.7852

Activity <TestBookmark> state is Canceled at 04:13:53.8008

{

    Arguments

        BookmarkName: TestBookmark

        Result: 0

}

Activity <CancellationScope> is scheduled child activity <CancelHandlerWriteLine> at 04:13:53.8008

Activity <CancelHandlerWriteLine> state is Executing at 04:13:53.8008

{

    Arguments

        Text: 

        TextWriter: 

}

Activity <CancelHandlerWriteLine> state is Closed at 04:13:53.8008

{

    Arguments

        Text: 

        TextWriter: 

}

Activity <CancellationScope> state is Canceled at 04:13:53.8008

WorkflowInstance for Activity <CancellationScope> state is <Canceled> at 04:13:53.8008