Integrationstest mit Bot Framework und DirectLine für Flow-Fälle

· 8 Min. Lesezeit

Einführung

In den vorherigen Blogbeiträgen haben wir einige Integrationstests für einzelne Fälle durchgeführt. „Einzelfälle“ sind die Fälle, in denen der Bot, nachdem er etwas gefragt hat, nur einmal antwortet und wir dann die Ergebnisse vergleichen.

Diese Anleitung erklärt nicht, wie die Authentifizierung und API-Aufrufe durchgeführt werden. Wenn Sie sie sich ansehen möchten, schauen Sie sich bitte die Einzelfallanleitung an.

Was ist mit Flow-Gesprächen?

Mit der derzeitigen Methode haben wir keinen fließenden Gesprächsfluss, wenn Sie beispielsweise um Hilfe bitten möchten und dann ein Menü mit verschiedenen Optionen angezeigt wird und nach Auswahl einer davon ein weiteres Menü angezeigt wird. Dies würde etwa 2 oder mehr responses ergeben. Das bedeutet also, dass die Verwendung der Integrationstestlösung, die wir zuvor verwendet haben, nicht funktionieren wird.

Hier ist ein Diagramm, wie der Integrationstest für Einzelfälle funktioniert.

https://gyazo.com/8915f2653033c1143947ef59196403f4

Und hier ist ein Diagramm, wie wir die aktuelle Integrationstestlösung an Flow-Fälle anpassen werden.

https://gyazo.com/ef77fa168d3b5f8b4a116ff38b5edd83

Die folgende Erklärung deckt nicht alle grundlegenden Informationen zur Funktionsweise des Bot Frameworks ab. Wenn Sie es nicht verstehen, schauen Sie sich bitte die offizielle Dokumentation an.

Beispielfall

Für die folgende Anleitung verwende ich einen Bot mit einer von mir erstellten Flow-Konversation, die um Hilfe bittet und dann verschiedene Optionen auswählt.

https://gyazo.com/0a1104cce67c07331a6e0fbd8e19b3e2

Neue JSON-Struktur

Da wir nun mehr als einen request haben, wenn wir mit dem Bot sprechen, müssen wir unsere JSON-Struktur ändern, um alle request hinzuzufügen, die wir tun werden.

{
  "secret": "direct-line-secret",
  "directlineGenerateTokenEndpoint":
    "https://directline.botframework.com/v3/directline/tokens/generate",
  "directlineConversationEndpoint":
    "https://directline.botframework.com/v3/directline/conversations/",
  "entries": [
    {
      "name": "PedirAyuda",
      "requests": [
        {
          "type": "message",
          "text": "Ayuda",
          "from": {
            "id": "default-user",
            "name": "User"
          },
          "locale": "es",
          "textFormat": "plain",
          "timestamp": "2018-04-09T08:04:37.195Z",
          "channelData": {
            "clientActivityId": "1523261059363.6264723268323733.0"
          },
          "entities": [
            {
              "type": "ClientCapabilities",
              "requiresBotState": true,
              "supportsTts": true,
              "supportsListening": true
            }
          ],
          "id": "61hacck8j6jg"
        },
        {
          "type": "message",
          "text": "Telefono",
          "from": {
            "id": "default-user",
            "name": "User"
          },
          "locale": "es",
          "textFormat": "plain",
          "timestamp": "2018-04-09T08:04:37.195Z",
          "channelData": {
            "clientActivityId": "1523261059363.6264723268323733.0"
          },
          "entities": [
            {
              "type": "ClientCapabilities",
              "requiresBotState": true,
              "supportsTts": true,
              "supportsListening": true
            }
          ],
          "id": "61hacck8j6jg"
        },
        {
          "type": "message",
          "text": "Oficina",
          "from": {
            "id": "default-user",
            "name": "User"
          },
          "locale": "es",
          "textFormat": "plain",
          "timestamp": "2018-04-09T08:04:37.195Z",
          "channelData": {
            "clientActivityId": "1523261059363.6264723268323733.0"
          },
          "entities": [
            {
              "type": "ClientCapabilities",
              "requiresBotState": true,
              "supportsTts": true,
              "supportsListening": true
            }
          ],
          "id": "61hacck8j6jg"
        },
        {
          "type": "message",
          "text": "Tenerife",
          "from": {
            "id": "default-user",
            "name": "User"
          },
          "locale": "es",
          "textFormat": "plain",
          "timestamp": "2018-04-09T08:04:37.195Z",
          "channelData": {
            "clientActivityId": "1523261059363.6264723268323733.0"
          },
          "entities": [
            {
              "type": "ClientCapabilities",
              "requiresBotState": true,
              "supportsTts": true,
              "supportsListening": true
            }
          ],
          "id": "61hacck8j6jg"
        }
      ],
      "response": {
        "type": "message",
        "timestamp": "2018-04-09T08:04:37.901Z",
        "localTimestamp": "2018-04-09T09:04:37+01:00",
        "serviceUrl": "http://localhost:50629",
        "channelId": "emulator",
        "from": {
          "id": "j98bbdf097a",
          "name": "Bot"
        },
        "conversation": {
          "id": "eabcie4be8ak"
        },
        "recipient": {
          "id": "default-user"
        },
        "locale": "es",
        "text": "922920252",
        "attachments": [],
        "entities": [],
        "replyToId": "61hacck8j6jg",
        "id": "47me557ikbf7"
      },
      "assert": "Request.Text == Response.Text"
    }
  ]
}

Wie Sie sehen, gibt es eine wichtige Änderung: request ist jetzt requests. Das bedeutet, dass wir jetzt ein List<Activity> anstelle eines einzelnen activity haben.

Neue Objekte

Zuvor hatten wir unsere Objekte für einzelne Fälle festgelegt: TestEntry und TestEntryCollection. Für Flow-Fälle erstellen wir neue Objekte: TestEntryFlow und TestEntryFlowCollection.

TestEntryFlow

Dieses Objekt gilt für jeden Eintrag, den wir in der Sammlung haben. Achten Sie darauf, dass das Requests-Objekt jetzt ein List<Activity> ist und nicht wie zuvor erwähnt ein einzelnes Activity.

Da wir den Bot mehrmals fragen, benötigen wir mehrere activities, die an die Konversation gesendet werden.

public class TestEntryFlow
{
    /// <summary>
    /// Entry name
    /// </summary>
    [JsonProperty("name")]
    public string Name { get; set; }
    /// <summary>
    /// Activity requested by the entry
    /// </summary>
    [JsonProperty("requests")]
    public List<Activity> Requests { get; set; }
    /// <summary>
    /// Activity response expected by the entry
    /// </summary>
    [JsonProperty("response")]
    public Activity Response { get; set; }
    /// <summary>
    /// Assert value in string
    /// </summary>
    [JsonProperty("assert")]
    public string Assert { get; set; }
}

TestEntiresCollection

Dieses Objekt enthält die relevanten Informationen für DirectLine wie den secret und die Endpunkte sowie die Liste der Entries, die wir testen werden.

Beachten Sie, dass es sich bei Entries nun um eine Liste von TestEntryFlow und nicht um TestEntry handelt.

public class TestEntryFlowCollection
{
    /// <summary>
    /// DirectLine Secret
    /// </summary>
    [JsonProperty("secret")]
    public string Secret { get; set; }
    /// <summary>
    /// Endpoint to get the token using the secret for DirectLine
    /// </summary>
    [JsonProperty("directlineGenerateTokenEndpoint")]
    public string DirectLineGenerateTokenEndpoint { get; set; }
    /// <summary>
    /// Endpoint for a conversation in DirectLine
    /// </summary>
    [JsonProperty("directlineConversationEndpoint")]
    public string DirectLineConversationEndpoint { get; set; }
    /// <summary>
    /// Entries list
    /// </summary>
    [JsonProperty("entries")]
    public List<TestEntryFlow> Entries { get; set; }
}

Erstellen des TestMethod für Flow-Fälle

Neuer Ablauf

Schauen Sie sich zunächst noch einmal das Diagramm an (es ist dasselbe, das ich oben gepostet habe).

https://gyazo.com/ef77fa168d3b5f8b4a116ff38b5edd83

Wie Sie sehen können, ist die Ablaufstruktur zur Durchführung des Tests im Großen und Ganzen dieselbe:

  1. Informieren Sie sich

  2. Authentifizieren

  3. Erstellen Sie ein GesprächUnd hier ändert sich, was sich ändert, jetzt müssen wir alle Anfragen mehrmals an die Konversation senden. Dazu müssen wir für jede Anfrage eine Schleife durchlaufen, sie an den Bot senden und dann die letzte Antwort mit unserer erwarteten Antwort vergleichen.

  4. Senden Sie alle Anfragen

  5. Erhalten Sie alle Nachrichten

  6. Erhalten Sie die neueste Antwort

  7. Vergleichen Sie mit der erwarteten Antwort

  8. Ergebnis bestätigen

Code

Zunächst müssen wir die Informationen aus der Akte entnehmen, das ist das Gleiche, was wir zuvor bei den Einzelfällen gemacht haben.

// Load entries from file
var path = System.IO.File.ReadAllText(@"C:\dataFlow.json");

// Deserialize to object
var data = JsonConvert.DeserializeObject<TestEntryFlowCollection>(path);

Jetzt müssen wir für jedes TestEntryFlow des data.entries eine Schleife ausführen, damit wir dem gleichen Ablauf folgen können, den wir in den einzelnen Fällen bis zum neuen Teil gemacht haben, wo wir den requests einschleifen.

/// Arrange with current requested values
string token, newToken, conversationId;
Activity latestResponse = new Activity();

/// Act for step

/// 1 - Get token using secret from DirectLine in BotFramework panel
token = Utils.uploadString<DirectLineAuth>(data.Secret, data.DirectLineGenerateTokenEndpoint, "").token;

/// 2 - Create a new conversation
var createdConversation = Utils.uploadString<DirectLineAuth>(token, data.DirectLineConversationEndpoint, "");

// This returns a new token and a conversationId
newToken = createdConversation.token;
conversationId = createdConversation.conversationId;

/// 3 - Send an activity to the conversation with new token and conversationId
string directlineConversationActivitiesEndpoint = data.DirectLineConversationEndpoint + conversationId + "/activities";

Der folgende Schritt ist ziemlich einfach: Wir müssen die entry.requests einschleifen und alle activity an die Konversation senden.

foreach (Activity step in entry.Requests)
{
    if (step.Type == ActivityTypes.Message)
    {
        /// Step
        Utils.uploadString<DirectLineAuth>(newToken, directlineConversationActivitiesEndpoint, JsonConvert.SerializeObject(step));

        /// 4 - Get all activities, we get a List<activity> and a watermark
        var getLastActivity = Utils.downloadString<ActivityResponse>(newToken, directlineConversationActivitiesEndpoint);

        /// 5 - Get the latest activity which is the response we should be expecting
        latestResponse = getLastActivity.activities[Int32.Parse(getLastActivity.watermark)];
    }
}

Wir verwenden den watermark, um die neueste Nachricht abzurufen. Der watermark ist ein Wert, den die DirectLine-API zurückgibt, wenn sie nach den Konversationsinformationen fragt.

Danach müssen wir nur noch den globals mit unserem latestReponse und expectedResponse füllen.

/// Arrange with new values
var globals = new Objects.Globals { Request = entry.Response, Response = latestResponse };

Und um den Fall abzuschließen, werten wir die assert-Zeichenfolge im entry aus.

/// Assert
Assert.IsTrue(await CSharpScript.EvaluateAsync<bool>(entry.Assert, globals: globals));

Endgültiger Code

[TestMethod]
public async Task ShouldTestFlowCases()
{
    // Load entries from file
    var path = System.IO.File.ReadAllText(@"C:\dataFlow.json");

    // Deserialize to object
    var data = JsonConvert.DeserializeObject<TestEntryFlowCollection>(path);

    /// Flow: Arrange -> Act -> arrange -> assert
    foreach (TestEntryFlow entry in data.Entries)
    {
        /// Arrange with current requested values
        string token, newToken, conversationId;
        Activity latestResponse = new Activity();

        /// Act for step

        /// 1 - Get token using secret from DirectLine in BotFramework panel
        token = Utils.uploadString<DirectLineAuth>(data.Secret, data.DirectLineGenerateTokenEndpoint, "").token;

        /// 2 -Create a new conversation
        var createdConversation = Utils.uploadString<DirectLineAuth>(token, data.DirectLineConversationEndpoint, "");

        // This returns a new token and a conversationId
        newToken = createdConversation.token;
        conversationId = createdConversation.conversationId;

        /// 3 - Send an activity to the conversation with new token and conversationId
        string directlineConversationActivitiesEndpoint = data.DirectLineConversationEndpoint + conversationId + "/activities";

        foreach (Activity step in entry.Requests)
        {
            if (step.Type == ActivityTypes.Message)
            {
                /// Step
                Utils.uploadString<DirectLineAuth>(newToken, directlineConversationActivitiesEndpoint, JsonConvert.SerializeObject(step));

                /// 4 - Get all activities, we get a List<activity> and a watermark
                var getLastActivity = Utils.downloadString<ActivityResponse>(newToken, directlineConversationActivitiesEndpoint);

                /// 5 - Get the latest activity which is the response we should be expecting
                latestResponse = getLastActivity.activities[Int32.Parse(getLastActivity.watermark)];
            }
        }

        /// Arrange with new values
        var globals = new Objects.Globals { Request = entry.Response, Response = latestResponse };

        /// Assert
        Assert.IsTrue(await CSharpScript.EvaluateAsync<bool>(entry.Assert, globals: globals));
    }

    await Task.CompletedTask;
}

Verbesserungen

Ich glaube zwar, dass ein besserer Ablauf möglich sein könnte, aber diese Verbesserung bedeutet, dass auch die JSON-Struktur geändert werden sollte. Um dies zu ermöglichen, muss der JSON außerdem besser gefüllt werden.

Um diese Tests zu verbessern, sollten wir für jedes request ein response haben und jedes Mal, wenn wir eine Nachricht senden, eine Bestätigung vornehmen. Im Moment machen wir das so, dass wir alle requests und die endgültigen response speichern.

Ich habe ein Diagramm erstellt, um zu zeigen, wie das aussehen würde.

https://gyazo.com/dfd4e9f87ff69159f02a0bcc70ae1edc

Ich bin der festen Überzeugung, dass dieser Weg insgesamt viel besser für die Integrität des Tests ist, da Sie so ziemlich jedes Verhalten im Fluss testen, anstatt nur die endgültige Antwort zu testen.


Nun, das ist alles für diesen Leitfaden. Bitte denken Sie daran, dass dieser Leitfaden eine Fortsetzung des Einzelfall-Leitfadens sein soll. Wenn Sie sich verloren fühlen, schauen Sie sich den Leitfaden an, der länger ist und mehr Erklärungen für alles enthält.

Denken Sie daran, dass der gesamte Code in meinem Github in diesem-Repository gespeichert ist.

Haben Sie einen guten Tag!