How do I test an activity with bookmarks?

Bookmarks allow an activity to inform the host application and workflow runtime that it needs more data before it can continue. The host application or some other workflow extension can resume the bookmark with the required data at some later point in time.

Activities with bookmarks must be tested using WorkflowApplicationTest host because WorkflowInvokerTest does not allow activities to use bookmarks.

Notes:

The Activity Library

This sample includes an Activity Library named UserActivityLibrary.  This project includes two activities.

A NativeActivity named ReadLine

public sealed class ReadLine : NativeActivity<string>
{
    [RequiredArgument]
    public InArgument<string> BookmarkName { get; set; }

    protected override bool CanInduceIdle
    {
        get { return true; }
    }

    protected override void Execute(NativeActivityContext context)
    {
        // Inform the host that this activity needs data and wait for the callback
        context.CreateBookmark(this.BookmarkName.Get(context), this.OnReadComplete);
    }

    private void OnReadComplete(NativeActivityContext context, Bookmark bookmark, object state)
    {
        // Store the value returned by the host
        context.SetValue(this.Result, state as string);
    }
}

A XAML Activity named ReadUser.xaml

The behavior of the activity is as follows

Given
An activity named ReadUser
Which prompts the user for a first name and then sets a bookmark
and when resumed prompts the user for a last name and then sets a bookmark
When
The activity is resumed after the second bookmark
Then
It writes a greeting to the console saying Hello <First Name> <Last Name>

ReadUser.png

The Test Project

The sample also includes a test project UserActivityLibrary.Tests

We used NuGet to Install Microsoft.Activities.UnitTesting to UserActivityLibrary.Tests

pmcUserActivityTests.png

The Test Code

When testing with bookmarks, you now have to deal with test timeouts. If your test thread is waiting for an idle event with the bookmark that never happens you need a timeout.
We added a timeout field to the test class. The length of the timeout should be long enough to allow the activity to complete its episode of work

public readonly TimeSpan TestTimeout = TimeSpan.FromMilliseconds(100);

Write the following test code to verify that the activity correctly pauses. This test code uses Workflow Episode support provided by extension methods from Microsoft.Activities. To illustrate what happens if there is a timeout waiting for a bookmark we have intentionally misspelled the FirstName bookmark

[TestMethod]
public void ReadUserShouldDisplayAGreeting()
{
    // Arrange
    const string firstBookmark = "FirsName";    // Intentional error  - bookmark name misspelled
    const string lastBookmark = "LastName";      
    const string expectedFirst = "First";
    const string expectedLast = "Last";
    const string expectedGreeting = "Hello " + expectedFirst + " " + expectedLast;
    var host = WorkflowApplicationTest.Create(new ReadUser());
    try
    {
        // Act
        // This code uses Workflow Episode support provided by extension methods from Microsoft.Activities

        // Run the workflow to the first idle point where a bookmark named "FirstName" exists
        host.TestWorkflowApplication.RunEpisode(firstBookmark, TestTimeout);

        // Resume the workflow with the first name
        host.TestWorkflowApplication.ResumeEpisodeBookmark(
            firstBookmark,      // The name of the bookmark to resume
            expectedFirst,      // The value to resume the bookmark with
            lastBookmark,       // The name of the next bookmark to wait for
            TestTimeout);    


        // Resume the workflow with the last name and run until complete, abort or timeout
        host.TestWorkflowApplication.ResumeEpisodeBookmark(lastBookmark, expectedLast, TestTimeout);

        // Assert
        // The text lines property captures text written with the WriteLine activity into an array of strings
        // There are three WriteLines plus an extra empty element at the end
        Assert.AreEqual(4, host.TextLines.Length);
        Assert.AreEqual(expectedGreeting, host.TextLines[2], "The greeting was not correct");
    }
    finally
    {
        host.Tracking.Trace();
    }
}

Task 1 - Run the Test

When you run the test, it will start the workflow and wait for an idle event with the bookmark named "FirsName" which will never occur so the workflow episode will throw a TimeoutException

Test method UserActivityLibrary.Tests.ReadUserActivityTest.ReadUserShouldDisplayAGreeting threw exception: 
System.TimeoutException: The operation has timed out.

Task 2 - Diagnose the Test Failure

The Debug Trace will contain the tracking information formatted to be human readable. Review the tracking data to determine the problem.

Did the workflow go idle with the correct bookmark name?

Record 9 shows that the BookmarkName property of the ReadLine activity was set to FirstName.
Record 10 shows that the last thing the workflow did was to become idle.
From this we are left with two possible choices

  1. The ReadLine activity has a bug in it (we should have a separate test for this)
  2. The test code has a bug in it.
*** Tracking data follows ***
0: WorkflowInstance "ReadUser" is Started at 11:40:23.2710
1: Activity [null] "null" scheduled child activity [1] "ReadUser" at 11:40:23.2710
2: Activity [1] "ReadUser" is Executing at 11:40:23.2710
3: Activity [1] "ReadUser" scheduled child activity [1.1] "Sequence" at 11:40:23.2710
4: Activity [1.1] "Sequence" is Executing at 11:40:23.2770
{
    Variables
        FirstName: 
        LastName: 
}
5: Activity [1.1] "Sequence" scheduled child activity [1.16] "WriteLine" at 11:40:23.2770
6: Activity [1.16] "WriteLine" is Executing at 11:40:23.2770
{
    Arguments
        Text: First Name:
        TextWriter: 
}
7: Activity [1.16] "WriteLine" is Closed at 11:40:23.2770
{
    Arguments
        Text: First Name:
        TextWriter: 
}
8: Activity [1.1] "Sequence" scheduled child activity [1.12] "ReadLine" at 11:40:23.2770
9: Activity [1.12] "ReadLine" is Executing at 11:40:23.2770
{
    Arguments
        BookmarkName: FirstName
}
10: WorkflowInstance "ReadUser" is Idle at 11:40:23.2780

Task 3 - Correct the test code

const string firstBookmark = "FirstName";