Automate Bot tests with saved transcript files

I’ve recently worked on a testing framework with Ben Williams (@negativeeddy) that allows you to write your own chat scripts. However, I thought that saving transcripts of your chat from the Bot Emulator would be a really helpful way to produce automated testing. Unfortunately I could not find any out-of-the-box support for running transcript files. Thus the following was born;

Testing with Transcript files

In my example I have the following chat conversation with my Adventure Bot;


USER
Examine table

BOT
The table is old but in good condition.
On the table there is a key 'Room Key A'.
ACTION
Take Room Key A

USER
Take key

BOT
You Take key.
ACTION
look
inventory

When you save the transcript file from the Emulator it contains the serialized version of those activities. Here’s a snippet of the above;

{
“type”: “message”,
“text”: “examine table”,
“from”: {
“id”: “5c8620e2-ccd8-4624-9c91-9842610220a4”,
“name”: “User”
},
{
“type”: “message”,
“serviceUrl”: “http://localhost:50090”,
“channelId”: “emulator”,
“from”: {
“id”: “1”,
“name”: “Bot”,
“role”: “bot”
},
“conversation”: {
“id”: “b1bbf220-011b-11e9-aa8d-d94902c621f5|livechat”
},
“recipient”: {
“id”: “5c8620e2-ccd8-4624-9c91-9842610220a4”,
“role”: “user”
},
“text”: “The table is old but in good condition.\nOn the table there is a key ‘Room Key A’.\n”,
“inputHint”: “acceptingInput”,
“suggestedActions”: {
“actions”: [
{
“type”: “imBack”,
“title”: “take Room Key A”,
“value”: “take Room Key A”
}
]
},

Write a test

Now we have the transcript file we want to use it from a test. First create a Test project and get the contents of the file. Note, I’m using MSTest and Moq. I’m taking a hacky short cut and reading the whole file but it should be using streams;


[TestMethod]
public async Task TakeKeyFromTranscriptTest()
{
  var transcriptFile = await File.ReadAllTextAsync("Transcripts\\examineTable.transcript");
  var activityTranscript = JsonConvert.DeserializeObject(transcriptFile);
  await TestBotTranscriptAsync(activityTranscript);
}

Set-up the test

Now we configure the test and the service-under-test, or in our case – a dialog.


private async Task TestBotTranscriptAsync(List activityTranscript, object request = null)
{
  // create mock version
  var mockDependency = new Mock();
  RecognizerResult recognizerResult = FakeRecognizerResult(
   "examine table",
   "Examine",
   "table");

  mockDependency.Setup(x => x.RecognizeAsync(
   It.Is(i => i.Activity.Text.Equals("Examine table", StringComparison.InvariantCultureIgnoreCase)),
   It.IsAny()))
   .Returns(Task.FromResult(recognizerResult));

  recognizerResult = FakeRecognizerResult(
   "take Room Key A",
   "Take",
   "key");

  // set up mock method
  mockDependency.Setup(x => x.RecognizeAsync(
   It.Is(i => i.Activity.Text.Equals("take Room Key A", StringComparison.InvariantCultureIgnoreCase)),
   It.IsAny()))
   .Returns(Task.FromResult(recognizerResult));

  this.Recognizer = mockDependency.Object;

  await CreateDialogTest(
   nameof(AdventureDialog),
   (id, svc, log) => new AdventureDialog(id, svc, log),
   options: request)
   .AssertBotTranscript(activityTranscript)
   .StartTestAsync();
}

private static RecognizerResult FakeRecognizerResult(string query, string intent, string noun)
{
    var resultJson = "{\"text\":\"" + query +
        "\",\"alteredText\":null,\"intents\":{\"" + intent +
        "\":{\"score\":0.9696778}},\"entities\":{\"$instance\":{\"noun\":[{\"startIndex\":8,\"endIndex\":13,\"text\":\"" + noun +
        "\",\"type\":\"noun\",\"score\":0.997288644}]},\"noun\":[\"" + noun + "\"]}}";
    var recognizerResult = JsonConvert.DeserializeObject(resultJson);
    return recognizerResult;
}

The Botness of the test is inside the CreateDialogTest method;


public TestFlow CreateDialogTest(string dialogId,
Func createDialog,
object options = null)
where TDialog : Dialog
{
  // setup the bot state for the test
  var storage = new MemoryStorage();
  var conversationState = new ConversationState(storage);
  var userState = new UserState(storage);

  var accessors = new AdventureCBCBotAccessors(conversationState, userState)
  {
   AdventureState = conversationState.CreateProperty(AdventureCBCBotAccessors.AdventureStateName),
   DialogState = conversationState.CreateProperty(AdventureCBCBotAccessors.DialogStateName),
  };

  // create the test adapter with any required middleware
  var adapter = new TestAdapter(sendTraceActivity: true)
   .Use(new GameServicesMiddleware(accessors.AdventureState))
   .Use(new AutoSaveStateMiddleware(conversationState));

  // Create or Moq any services
  ILoggerFactory factory = new LoggerFactory()
   .AddConsole()
   .AddDebug();

  var services = new BotServices(
   new BotConfiguration(),
   TestEndpoint
  );

  if (this.Recognizer != null)
  {
   services.LuisServices.Add("LuisAdventureBot", this.Recognizer);
  }

  // Create the dialog under test
  var dialogState = conversationState.CreateProperty("dialogState");
  var dialogs = new DialogSet(dialogState);
  TDialog dialog = createDialog.Invoke(dialogId, services, factory);
   dialogs.Add(dialog);

  // Start the test flow-loop to execute the chat
  return new TestFlow(adapter, async (turnContext, cancellationToken) =>
  {
   var dc = await dialogs.CreateContextAsync(turnContext, cancellationToken);

   var result = await dc.ContinueDialogAsync(cancellationToken);

   if (result.Status == DialogTurnStatus.Complete)
   {
    if (result.Result != null)
    {
     await turnContext.SendActivityAsync(result.Result.ToString());
    }
    return;
   }

   if (!turnContext.Responded)
   {
    await dc.BeginDialogAsync(dialog.Id, options, cancellationToken);
   }
  });

public EndpointService TestEndpoint { get; set; } = new EndpointService() { Endpoint = "https://example.com/bot" };
}

Execute a test

You may have noticed the lines;


.AssertBotTranscript(activityTranscript)
.StartTestAsync();

These are were the execution and testing happen. StartTestAsync start the test adapter but the AssertBotTranscript activity extension is where the magic happens. Before we look at that we need to create a couple of helper classes. These classes are responsible for finding all the message activies and to identify which ones are from the user or the bot.


public enum StepActor
{
  User,
  Bot
}

public class BotScript
{

  public BotScript(List transcript)
  {
   this.Steps = this.ParseScript(transcript);
  }

  public string UserName { get; set; } = "User";

  public IEnumerable Steps { get; }

  private IEnumerable ParseScript(List transcript)
  {
   foreach (var activity in transcript
    .SkipWhile(t => t.Type != ActivityTypes.Message || t.From.Name != this.UserName)
    .Where(t => t.Type == ActivityTypes.Message))
   {
    if (activity.From.Name == this.UserName)
    {
     BotStep step = new BotStep
     {
       Actor = StepActor.User,
       Text = activity.Text,
     };

     yield return step;
    }
    else
    {
     BotStep step = new BotStep
     {
       Actor = StepActor.Bot,
       Text = activity.Text,
       Actions = activity.SuggestedActions?.Actions.Select(a => a.Title).ToList(),
     };

     yield return step;
    }
   }
 }
}

public class BotStep
{
  public StepActor Actor { get; internal set; }
  public string Text { get; internal set; }
  public List Actions { get; internal set; }
}

Now we have our User and Bot steps we can execute the test. This is achieved by enumerating all the steps from the transcripts. If it is a User step then we send it into the Flow. If it’s a Bot step then we assert that the Bot has responded with the expected value in the step.


public static TestFlow AssertBotTranscript(this TestFlow testFlow, List transcript)
{
  var botScript = new BotScript(transcript);
  return testFlow.AssertBotScript(botScript);
}

public static TestFlow AssertBotScript(this TestFlow testFlow, BotScript script)
{
  foreach (var step in script.Steps)
  {
   if (step.Actor == StepActor.Bot)
   {
     testFlow = testFlow.AssertBotStep(step);
   }
   else
   {
     testFlow = testFlow.Send(step.Text);
   }
  }
 return testFlow;
}

public static TestFlow AssertBotStep(this TestFlow testFlow, BotStep expectedResponse)
{
 return testFlow.AssertReply(activity =>
 {
  var messageActivity = activity.AsMessageActivity();

  // check main text of message
  if (!string.IsNullOrEmpty(expectedResponse.Text))
  {
   string fixedExpected = SanitizeText(expectedResponse.Text);
   string actualResponse = SanitizeText(messageActivity.Text);

   Assert.AreEqual(fixedExpected, actualResponse);
  }
}
}

private static string SanitizeText(string text)
{
  string fixedReply = text;

  if (!string.IsNullOrEmpty(fixedReply))
  {
   fixedReply = fixedReply.Replace("\r", string.Empty).Replace("\n", string.Empty);
  }

  return fixedReply;
}

All that’s left to is to run the test;

Capture

 

It is just a start

I’ve left out of lot of additional code you need, e.g. handling Adaptive cards, Actions, Hero Cards, etc. but it’s exactly the same idea. You need to flesh out the BotStep class and AssertReply to handle the different forms of message activities. Hopefully, this will just be included in the Framework of the Bot Community framework and you will never need to write this yourself. But for now, this should help you out.

Can’t Render Card – Bot Framework Emulator

I was having a strange problem with rendering Adaptive Cards today, the emulator just kept complaining that it couldn’t render the card. I’m using the latest Adaptive Card package (1.1) so I couldn’t see why they’d be a problem. Turns out the emulator currently only supports 1.0. So when you create a card you have to do something like;

AdaptiveCard var = new AdaptiveCard(new AdaptiveSchemaVersion(1, 0));

Testing using ConfigurationManager with .net core

.net core has moved away from web.config and app.config but realistically it’s a pattern that many projects will still want to follow. Fortunately there is help in the form of the System.Configuration.ConfigurationManager NuGet package. However, there is a snag when it comes to using this package with unit testing, it doesn’t work 😉 The problem is that it goes looking for a .config file related to the test host rather than your test project. I’m currently working around this with a quick static hack but the correct answer is to inject a new config. You can correct the path like this;

var myConfig = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None).FilePath;            
myConfig = myConfig.Replace("testhost.dll.config", "MyProjectTests.dll");
var config = ConfigurationManager.OpenExeConfiguration(myConfig);

Hope that helps.

Prevent PromptValidator from showing a prompt

The PromptValidator in the Bot Framework is a powerful mechanism for collecting form information. However, one slight snag with it is that it only allows a limited control of the flow, i.e. you can say true/false and you can alter the (re)prompt. In certain situations you may want the validator to restart your dialog. That’s relatively easy, you just call dialogContext.ReplaceDialogAsync(this.Id). The problem is that whilst the framework will honor the replace request it will also continue with the retry prompt, almost certainly not what you want. To workaround this problem you provide a non-textual prompt and everything appears good to the user. E.g.

promptContext.Options.Prompt = new Activity(type: ActivityTypes.Typing);

Call overhead with large number of dialogs in Bot Framework

When you look at the samples for the Bot Framework you’ll no doubt see a pattern for the root dialog that looks like this;

public MyBot(MyBotAccessors accessors, ILoggerFactory loggerFactory)
{
    Dialogs = new DialogSet(accessors.DialogState);
    Dialogs.Add(new DialogA());
    Dialogs.Add(new DialogB());
    ...
    Dialogs.Add(new DialogN());
}

The great thing about the above pattern is that it is nice and clean, you know that the dialog set will be correctly initialized and therefore when you call var dialogResult = await dialogContext.ContinueDialogAsync or await dialogContext.BeginDialogAsync everything will just work.

The problem with the above is that the MyBot constructor will be called for every message that the bot receives. Therefore, every new DialogX will also be called. If you have a reasonably feature rich bot then this overhead might become a problem, especially if the constructors are doing anything…I mean you never write your constructor to do anything slow…right? Regardless I wanted to see if the overhead could be avoided…it can.

public MyBot(MyBotAccessors accessors, ILoggerFactory loggerFactory)
{
    // Initialize like before
    Dialogs = new DialogSet(accessors.DialogState);
}

public async Task OnTurnAsync(ITurnContext turnContext)
{
    ...
    // (skipping code to focus)
    var dialogContext = await Dialogs.CreateContextAsync(turnContext);

    // the context is loaded from state, regardless of our instances
    // so we can see what the Active Dialog Id and...
    if (dialogContext.ActiveDialog?.Id == "DialogB")
    {
        // create in JIT 
        Dialogs.Add(new DialogB());
    }
    ...

    // remember you'll also have to JIT to invoke the dialog
    // don't add it twice
    Dialogs.Add(new DialogB());
    await dialogContext.BeginDialogAsync(nameof(DialogB));
}

Extracting an Entity from LUIS in Bot Framework

Recent changes to the supporting LUIS libraries in the Bot Framework have puzzled me. I find that whilst getting the Top Intent from the RecognizerResult recognizerResult?.GetTopScoringIntent(); is easy, getting hold of an Entity seems very strange. I’m not sure if it’s my SDK, LUIS models or what, but this is the code I’m currently having to use…and I confess I still find it all very odd [Edit 20-Nov-2018] Code changed to better cope with arrays coming and going :s;

public static T GetEntity<T>(this RecognizerResult luisResult, string entityKey, string valuePropertyName = "text")
{
    if (luisResult != null)
    {
        //// var value = (luisResult.Entities["$instance"][entityKey][0]["text"] as JValue).Value;
        var data = luisResult.Entities as IDictionary<string, JToken>;

        if (data.TryGetValue("$instance", out JToken value))
        {
            var entities = value as IDictionary<string, JToken>;
            if (entities.TryGetValue(entityKey, out JToken targetEntity))
            {
                var entityArray = targetEntity as JArray;
                if (entityArray.Count > 0)
                {
                    var values = entityArray[0] as IDictionary<string, JToken>;
                    if (values.TryGetValue(valuePropertyName, out JToken textValue))
                    {
                        var text = textValue as JValue;
                        if (text != null)
                        {
                            return (T)text.Value;
                        }
                    }
                }
            }
        }
    }

    return default(T);
}

Hope it helps

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();
}

Also see; Automate Bot Tests with saved transcript files

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.

Also see my tutorial on this subject; non accessor state in v4

“Prompt options are required for Prompt dialogs” in Bot Framework

A quick reminder style post. If you get this strange sounding exception; Prompt options are required for Prompt dialogs

Then you’ve probably made the same mistake I did and forgot to add the waterfall steps to the dialogset;

 var waterfallSteps = new WaterfallStep[]
            {
                    InitializeStateStepAsync,
                    PromptForDescriptionStepAsync,
                    PromptForTopicStepAsync,
                    DisplaySummaryStepAsync,
            };
// * forgot to add this line 
// AddDialog(new WaterfallDialog("DescAndTopicDialog", waterfallSteps));
AddDialog(new TextPrompt(DescriptionPrompt, ValidateDescriptionAsync));
AddDialog(new TextPrompt(TopicPrompt, ValidateTopicAsync));