Prueba de integración usando Bot Framework y DirectLine para casos de flujo

· 8 min de lectura

Introducción

En publicaciones de blog anteriores, hicimos algunas pruebas de integración para casos individuales. Los “casos únicos” son aquellos casos en los que después de preguntarle algo al bot, este responderá solo una vez y luego comparamos los resultados.

Esta guía no explica cómo se realizan la autenticación y las llamadas API. Si desea consultarlo, consulte la guía de casos individuales.

¿Qué pasa con las conversaciones fluidas?

Usando la forma actual no tenemos ningún tipo de flujo en la conversación, por ejemplo, si quieres pedir ayuda y luego aparece un menú con diferentes opciones, y luego de seleccionar una de ellas, otro menú. Esto haría como 2 o más responses. Esto significa que usar la solución de prueba de integración que usamos antes no funcionará.

A continuación se muestra un diagrama de cómo funciona la prueba de integración para casos individuales.

https://gyazo.com/8915f2653033c1143947ef59196403f4

Y aquí hay un diagrama de cómo vamos a adaptar la solución de prueba de integración actual a los casos de flujo.

https://gyazo.com/ef77fa168d3b5f8b4a116ff38b5edd83

La siguiente explicación no cubrirá toda la información básica sobre cómo funciona Bot Framework. Si no lo comprende, consulte la documentación oficial.

Caso de ejemplo

Para la siguiente guía, usaré un bot con una conversación fluida creada por mí, que pide ayuda y luego selecciona diferentes opciones.

https://gyazo.com/0a1104cce67c07331a6e0fbd8e19b3e2

Nueva estructura JSON

Ahora que tenemos más de un request al hablar con el bot, necesitamos modificar nuestra estructura json para agregar todos los request que estaremos haciendo.

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

Como puedes ver, hay un cambio importante, request ahora es requests. Eso significa que ahora tenemos un List<Activity> en lugar de un único activity.

Nuevos objetos

Anteriormente teníamos nuestros objetos configurados para casos únicos: TestEntry y TestEntryCollection. Para casos de flujo crearemos nuevos objetos: TestEntryFlow y TestEntryFlowCollection.

TestEntryFlow

Este objeto es para cada entrada que tenemos en la colección, mira que el objeto Requests ahora es un List<Activity> en lugar de un solo Activity como mencioné antes.

Dado que le preguntaremos al bot varias veces, necesitamos tener varios activities que se enviarán a la conversación.

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

Este objeto contendrá la información relevante para DirectLine como el secret y los puntos finales, además de la lista de Entries que estaremos probando.

Tenga en cuenta que el Entries ahora es una lista de TestEntryFlow y no 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; }
}

Creando el TestMethod para casos de flujo

Nuevo flujo

En primer lugar, eche un vistazo nuevamente al diagrama (es el mismo que publiqué arriba).

https://gyazo.com/ef77fa168d3b5f8b4a116ff38b5edd83

Como puedes ver, la estructura de flujo para realizar la prueba es prácticamente la misma:

  1. Obtener información

  2. Autenticar

  3. Crea una conversaciónY aquí lo que cambia, ahora tenemos que enviar varias veces todas las solicitudes a la conversación. Para hacer esto, tenemos que realizar un bucle para cada solicitud, enviarla al bot y luego comparar la última respuesta con nuestra respuesta esperada.

  4. Enviar todas las solicitudes

  5. Recibe todos los mensajes

  6. Obtenga la última respuesta

  7. Comparar con la respuesta esperada

  8. Afirmar el resultado

Código

En primer lugar, necesitamos obtener la información del archivo, esto es lo mismo que hicimos antes con los casos individuales.

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

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

Ahora tenemos que hacer un bucle para cada TestEntryFlow, del data.entries, con eso podemos seguir el mismo flujo que hicimos en los casos individuales hasta la nueva parte, donde hacemos un bucle en el 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";

El siguiente paso es bastante simple, tenemos que realizar un bucle en el entry.requests y enviar cada activity a la conversación.

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

Usamos watermark para obtener el último mensaje, watermark es un valor que la API de DirectLine devuelve cuando solicita la información de la conversación.

Después de eso, solo nos queda llenar el globals con nuestro latestReponse y expectedResponse.

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

Y para finalizar el caso, evaluamos la cadena assert en el entry.

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

Código 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;
}

Mejoras

Creo que podría ser posible un mejor flujo, pero esta mejora significará que la estructura JSON también debería cambiarse. Además, para que esto suceda, el json debe estar más lleno.

Para mejorar esta prueba, deberíamos tener un response para cada request y deberíamos afirmarlo cada vez que enviemos un mensaje. La forma en que lo estamos haciendo ahora es almacenando todos los requests y el [[TOK_56]]] final.

Hice un diagrama para mostrar cómo se vería esto.

https://gyazo.com/dfd4e9f87ff69159f02a0bcc70ae1edc

Creo firmemente que de esta manera es mucho mejor en general para la integridad de la prueba, ya que se prueban prácticamente todos los comportamientos en el flujo en lugar de simplemente probar la respuesta final.


Bueno, eso es todo por esta guía, recuerde que esta guía pretende ser una continuación de la guía de casos individuales, si se siente perdido, consulte esa guía que es más larga y tiene más explicaciones para todo.

Recuerde que todo el código está almacenado en mi github en este repositorio.

¡Que tenga un buen día!