Integrationstest mit Bot Framework und DirectLine für Flow-Fälle
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.
Und hier ist ein Diagramm, wie wir die aktuelle Integrationstestlösung an Flow-Fälle anpassen werden.
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.
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).
Wie Sie sehen können, ist die Ablaufstruktur zur Durchführung des Tests im Großen und Ganzen dieselbe:
Informieren Sie sich
Authentifizieren
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.
Senden Sie alle Anfragen
Erhalten Sie alle Nachrichten
Erhalten Sie die neueste Antwort
Vergleichen Sie mit der erwarteten Antwort
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.
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!



