Test d'intégration à l'aide de Bot Framework et DirectLine pour les cas de flux

· 8 min de lecture

##Présentation

Dans les articles de blog précédents, nous avons effectué des tests d’intégration pour des cas uniques. Les “cas uniques” sont les cas dans lesquels après avoir demandé quelque chose au robot, il ne répondra qu’une seule fois, puis nous comparerons les résultats.

Ce guide n’expliquera pas comment s’effectuent l’authentification et les appels API. Si vous souhaitez le consulter, veuillez consulter le guide des cas uniques.

Qu’en est-il des conversations de flux ?

En utilisant la méthode actuelle, nous n’avons aucun type de flux dans la conversation, par exemple, si vous souhaitez demander de l’aide, un menu s’affiche avec différentes options, puis après en avoir sélectionné une, un autre menu. Cela ferait environ 2 responses ou plus. Cela signifie donc que l’utilisation de la solution de test d’intégration que nous avons utilisée auparavant ne fonctionnera pas.

Voici un schéma du fonctionnement du test d’intégration pour les cas uniques.

https://gyazo.com/8915f2653033c1143947ef59196403f4

Et voici un schéma de la façon dont nous allons adapter la solution de test d’intégration actuelle aux cas de flux.

https://gyazo.com/ef77fa168d3b5f8b4a116ff38b5edd83

** L’explication suivante ne couvrira pas toutes les informations de base sur le fonctionnement du Bot Framework. Si vous ne comprenez pas, veuillez consulter la documentation officielle. **

Exemple de cas

Pour le guide suivant, j’utiliserai un bot avec une conversation de flux créée par moi, qui demande de l’aide puis sélectionne différentes options.

https://gyazo.com/0a1104cce67c07331a6e0fbd8e19b3e2

Nouvelle structure JSON

Maintenant que nous avons plus d’un request lorsque nous parlons au bot, nous devons modifier notre structure json pour ajouter tous les request que nous allons faire.

{
  "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"
    }
  ]
}

Comme vous pouvez le constater, il y a un changement important, request est désormais requests. Cela signifie que nous avons maintenant un List<Activity> au lieu d’un seul activity.

Nouveaux objets

Auparavant, nos objets étaient définis pour des cas uniques : TestEntry et TestEntryCollection. Pour les cas de flux, nous créerons de nouveaux objets : TestEntryFlow et TestEntryFlowCollection.

TestEntryFlow

Cet objet est pour chaque entrée que nous avons dans la collection, regardez que l’objet Requests est maintenant un List<Activity> au lieu d’un seul Activity comme je l’ai mentionné précédemment.

Puisque nous demanderons au bot plusieurs fois, nous devons avoir plusieurs activities qui seront envoyés à la conversation.

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

Cet objet contiendra les informations pertinentes pour DirectLine comme le secret et les points de terminaison, ainsi que la liste des Entries que nous allons tester.

Notez que le Entries est désormais une liste de TestEntryFlow et non de TestEntry.

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

Création du TestMethod pour les cas de flux

Nouveau flux

Tout d’abord, revoyez le schéma (c’est le même que celui que j’ai posté ci-dessus).

https://gyazo.com/ef77fa168d3b5f8b4a116ff38b5edd83

Comme vous pouvez le constater, la structure du flux pour effectuer le test est à peu près la même :

  1. Obtenez des informations

  2. Authentifier

  3. Créez une conversationEt voilà ce qui change, nous devons maintenant envoyer plusieurs fois toute la demande à la conversation. Pour ce faire, nous devons boucler chaque requête, l’envoyer au bot, puis comparer la dernière réponse à notre réponse attendue.

  4. Envoyez toutes les demandes

  5. Recevez tous les messages

  6. Obtenez la dernière réponse

  7. Comparez avec la réponse attendue

  8. Affirmer le résultat

###Code

Tout d’abord, nous devons extraire les informations du dossier, c’est la même chose que nous avons fait auparavant avec les cas individuels.

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

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

Maintenant, nous devons boucler pour chaque TestEntryFlow, des data.entries, avec cela nous pouvons suivre le même flux que nous avons fait dans les cas individuels jusqu’à la nouvelle partie, où nous bouclons dans le requests.

/// 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";

L’étape suivante est assez simple, nous devons boucler le entry.requests et envoyer chaque activity à la conversation.

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

Nous utilisons le watermark pour obtenir le dernier message, le watermark est une valeur que l’API DirectLine renvoie lors de la demande d’informations sur la conversation.

Après cela, il ne nous reste plus qu’à remplir le globals avec nos latestReponse et expectedResponse.

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

Et pour terminer le cas, nous évaluons la chaîne assert dans le entry.

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

Code final

[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;
}

Améliorations

Je pense qu’un meilleur flux pourrait être possible, mais cette amélioration signifie que la structure JSON devrait également être modifiée. De plus, pour que cela se produise, le json doit être davantage rempli.

Afin d’améliorer ces tests, nous devrions avoir un response pour chaque request, et nous devrions l’affirmer à chaque fois que nous envoyons un message. La façon dont nous procédons actuellement consiste à stocker tous les requests et le final response .

J’ai fait un schéma pour montrer à quoi cela ressemblerait.

https://gyazo.com/dfd4e9f87ff69159f02a0bcc70ae1edc

Je pense fermement que cette méthode est globalement bien meilleure pour l’intégrité du test, puisque vous testez à peu près tous les comportements du flux au lieu de simplement tester la réponse finale.


Eh bien, c’est tout pour ce guide, n’oubliez pas que ce guide est censé être une continuation du guide des cas uniques, si vous vous sentez perdu, consultez ce guide qui est plus long et contient plus d’explications sur tout.

N’oubliez pas que tout le code est stocké dans mon github dans le référentiel this.

Passe une bonne journée!