How to write a unit test for Bot Framework v4

I’ve previously written a quick post showing a unit test for v4 preview release. I thought I’d update it.

Goal

Unit Test a dialog we’ve written that asks for a user first and second name.

Steps for creating a unit Test

  1. Create a unit test project in your solution, I chose an MS Test project
  2. Using Nuget install Microsoft.Bot.Builder.Dialogs into the MS Test project
  3. Reference your Bot project
  4. Create a unit test, remember to make it async Task
[TestMethod]
public async Task CreatingAGoodContact()
{
    var convoState = new ConversationState(new MemoryStorage());

    var adapter = new TestAdapter()
        .Use(new AutoSaveStateMiddleware(convoState));

    var dialogState = convoState.CreateProperty("dialogState");

    var dialogs = new DialogSet(dialogState);
    dialogs.Add(new CreateContactDialog(null));

    await new TestFlow(adapter, async (turnContext, cancellationToken) =>
    {
        var dc = await dialogs.CreateContextAsync(turnContext, cancellationToken);
        await dc.ContinueDialogAsync(cancellationToken);
        if (!turnContext.Responded)
        {
            await dc.BeginDialogAsync("CreateContactDialog", null, cancellationToken);
        }
    })
    .Send("Say something to start test")
    .AssertReply("What is their first name?")
    .Send("Jane")
    .AssertReply("What is their last name?")
    .Send("Tan")
    .AssertReply($"I have created a contact called Jane Tan")
    .StartTestAsync();
}
Advertisements

Practical Dialog State in large Bot Framework v4 projects

State seems to be large topic in v4. I’ve written posts about it before, including; Waterfall Step Patterns and Changes in State Handling, but after more exposure to v4 I coming around to the decision that for more complex projects I’m going to recommend reverting back to how v3 manages state…mostly.

The v4 way, or not?

Using the Accessor Pattern for state is great, it provides a sense of concrete design. You create a dialog, you create one or more state accessors that will be passed to it. The use of the state is very explicit. This is all good. So what’s the problem? The problem is that for complex bot projects your state is going to be filled with little islands of seemingly unrelated data. Consider a simple scenario of a New Contact dialog. You invoke the dialog, which in-turn (pun not intended) invokes a New Address dialog which can also be used from other dialogs. You would write something like;

IStatePropertyAccessor contactStatePropertyAccessor;
IStatePropertyAccessor addressStatePropertyAccessor;

But in a complex Bot Project you may have tens or hundreds of dialogs;

IStatePropertyAccessor s1StatePropertyAccessor;
IStatePropertyAccessor s2StatePropertyAccessor;
...
IStatePropertyAccessor sNStatePropertyAccessor;

That’s not cool.

What about a Generic State Accessor?

So given we want to mitigate all these state islands, could we have a generic accessor? Short answer is, “yes”, actual answer is, “too dirty”.

IStatePropertyAccessor activeDialogStatePropertyAccessor;

Every dialog gets passed the same single accessor and they store their data in it. Ah but what our chain of dialogs, when we invoke our New Address won’t that overwrite the current New Contact state? Yes. So we’ll have to look at creating some form of dictionary/hash to avoid…hang on, this seems a lot like the Active Dialog state from v3. Is that still available?

Active Dialog State

Active Dialog State is the framework supplied state that follows the dialog stack. Or to put it another way, we don’t have to worry about the dialog stack. So how do we gain access to this in v4? Overrides;

public override Task BeginDialogAsync(DialogContext outerDc, object options = null, CancellationToken cancellationToken = default(CancellationToken))
{
    this.dialogContext = outerDc;
    this.SaveState();
    return base.BeginDialogAsync(outerDc, options, cancellationToken);
}

public override Task ContinueDialogAsync(DialogContext outerDc, CancellationToken cancellationToken = default(CancellationToken))
{
    this.dialogContext = outerDc;
    this.InitializeFromState();
    return base.ContinueDialogAsync(outerDc, cancellationToken);
}

In this example I’ve duplicated the handling of the state per class, but making this generic with clones or actions would be the way forward;

private void SaveState()
{
    // EDIT: DO NOT use 'this', its like creating a mini sub stack
    // and confuses the dialog rehydration. Use smaller struct/class
    this.dialogContext.ActiveDialog.State["ObjectState"] = this;
}

private void InitializeFromState()
{
    if (this.dialogContext.ActiveDialog.State.TryGetValue("ObjectState", out object rawObject))
    {
        var dialog = contactDialogObject as NewContactDialog;
        if (dialog != null)
        {
            if (dialog != null)
            {
                this.FirstName = dialog.FirstName;
                this.Telephone = dialog.Telephone;
            }
        }
    }
}

So now when the dialog starts it fetches its own state from the Active Dialog (itself), and updates its properties. So we are now at roughly the same point we would be in v3, i.e. we’ve been re-hydrated. We now need to update the state when a values changes;

private async Task PromptForTelephoneStepAsync(
                                        WaterfallStepContext stepContext,
                                        CancellationToken cancellationToken)
{
    // we've passed the validation so save the result
    this.Name = stepContext.Result.ToString();
    this.SaveState();
    ...

That’s it. We now have a fully working state engine for dialogs that doesn’t leave us with lots of data islands. Note, the above can be tidied up through the use of inheritance, generics, etc. Also note that you would need to implement other overrides to support restarting the flow, etc.

I should caution that I don’t know about the limits of state, perhaps I will discover a size restriction, we’ll see.

Creating a reusable TextPrompt in Bot Framework V4

The TextPrompt mechanism in V4 is fine and can implement a variety of validation techniques because it uses a delegate. However, delegates can create a lot of code noise, especially if you have a validation mechanism that you wish to reuse. Consider the following ‘Hello World’ of Waterflow steps;

public MyBot()
{
    dialogs = new DialogSet();
    dialogs.Add("greetings", new WaterfallStep[]
    {
        async (dc, args, next) =>
        {
            // Prompt for the guest's name.
            await dc.Prompt("textPrompt","What is your name?");
        },
        async(dc, args, next) =>
        {
            // args; Value: "<name>", Text: "<name>"
            var userResponse = args["Text"] as string;
            await dc.Context.SendActivity($"Hi {args["Text"]}!");
            await dc.End();
        }
    });

    // add the prompt, of type TextPrompt
    dialogs.Add("textPrompt", new Microsoft.Bot.Builder.Dialogs.TextPrompt(TextValidation));
}

private async Task TextValidation(ITurnContext context, TextResult toValidate)
{
    if (toValidate.Text.Length < 4)
    {
        toValidate.Status = null;
        await context.SendActivity("Sorry needs to be > 4");
    }
}

The problem is that the TextValidation delegate is ugly to re-use. I.e. I want a nicer way to share a simple length validation. This is my solution;

public class ValidatingTextPrompt : Microsoft.Bot.Builder.Dialogs.TextPrompt
{

    public static ValidatingTextPrompt Create(int minimumLength, string minimumLengthMessage)
    {
        var obj = new ValidatingTextPrompt(async (context, toValidate) =>
            {
                if (toValidate.Text.Length < minimumLength)
                {
                    toValidate.Status = null;
                    await context.SendActivity(minimumLengthMessage);
                }
            }
            );
        return obj;
    }

    public ValidatingTextPrompt(PromptValidatorEx.PromptValidator<TextResult> validator) : base(validator)
    {
    }
}

Then you can swap out the TextPrompt with the more specialized code;

dialogs.Add("textPrompt", ValidatingTextPrompt.Create(5, "I can't remember such a short name, please try again"));

If you have thoughts about a better way then please feel free to comment.