Teste de integração usando Bot Framework e DirectLine para casos de fluxo

· 8 min de leitura

Introdução

Nas postagens anteriores do blog, fizemos alguns testes de integração para casos únicos. “Casos únicos” são aqueles casos que após perguntar algo ao bot, ele responderá apenas uma vez e depois compararemos os resultados.

Este guia não explica como são feitas a autenticação e as chamadas de API. Se você quiser conferir, consulte o guia de casos únicos.

E as conversas de fluxo?

Usando a forma atual não temos nenhum tipo de fluxo na conversa, por exemplo, se você quiser pedir ajuda e aí aparece um menu com diversas opções, e depois de selecionar uma delas, outro menu. Isso daria cerca de 2 ou mais responses. Então, isso significa que usar a solução de teste de integração que usamos antes não funcionará.

Aqui está um diagrama de como funciona o teste de integração para casos únicos.

https://gyazo.com/8915f2653033c1143947ef59196403f4

E aqui está um diagrama de como vamos adaptar a atual solução de teste de integração aos casos de fluxo.

https://gyazo.com/ef77fa168d3b5f8b4a116ff38b5edd83

A explicação a seguir não cobrirá todas as informações básicas de como funciona o Bot Framework. Se você não entender, consulte a documentação oficial.

Exemplo de caso

Para o guia a seguir, usarei um bot com um fluxo de conversa criado por mim, que pede ajuda e depois seleciona diferentes opções.

https://gyazo.com/0a1104cce67c07331a6e0fbd8e19b3e2

Nova estrutura JSON

Agora que temos mais de um request ao falar com o bot, precisamos modificar nossa estrutura json para adicionar todos os request que iremos fazer.

{
  "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 você pode ver, há uma mudança importante, request agora é requests. Isso significa que agora temos um List<Activity> em vez de um único activity.

Novos objetos

Anteriormente tínhamos nossos objetos definidos para casos únicos: TestEntry e TestEntryCollection. Para casos de fluxo estaremos criando novos objetos: TestEntryFlow e TestEntryFlowCollection.

TestEntryFlow

Este objeto é para cada entrada que temos na coleção, veja que o objeto Requests agora é um List<Activity> em vez de um único Activity como mencionei antes.

Como perguntaremos ao bot várias vezes, precisamos ter vários activities que serão enviados para a conversa.

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 conterá as informações relevantes para DirectLine como o secret e os endpoints, além da lista de Entries que iremos testar.

Observe que, o Entries agora é uma lista de TestEntryFlow e não 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; }
}

Criando o TestMethod para casos de fluxo

Novo fluxo

Em primeiro lugar, dê uma olhada novamente no diagrama(é o mesmo que postei acima).

https://gyazo.com/ef77fa168d3b5f8b4a116ff38b5edd83

Como você pode ver, a estrutura do fluxo para fazer o teste é praticamente a mesma:

  1. Obtenha informações

  2. Autenticar

  3. Crie uma conversaE aqui o que muda, agora temos que enviar várias vezes toda a solicitação para a conversa. Para fazer isso, precisamos fazer um loop para cada solicitação, enviá-la ao bot e comparar a resposta mais recente com a resposta esperada.

  4. Envie todas as solicitações

  5. Receba todas as mensagens

  6. Obtenha a resposta mais recente

  7. Compare com a resposta esperada

  8. Afirmar resultado

Código

Em primeiro lugar, precisamos obter as informações do arquivo, é o mesmo que fizemos antes com os casos únicos.

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

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

Agora temos que fazer um loop para cada TestEntryFlow, do data.entries, com isso podemos seguir o mesmo fluxo que fizemos nos casos únicos até a nova parte, onde fazemos o loop no 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";

A etapa a seguir é bem simples, temos que fazer um loop no entry.requests e enviar cada activity para a conversa.

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 o watermark para obter a última mensagem, o watermark é um valor que a API DirectLine retorna ao solicitar as informações da conversa.

Depois disso, só temos que preencher o globals com nosso latestReponse e expectedResponse.

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

E para finalizar o caso, avaliamos a string assert no 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;
}

Melhorias

Acredito que um fluxo melhor poderia ser possível, mas essa melhoria significará que a estrutura JSON também deve ser alterada. Além disso, para que isso aconteça, o json precisa estar mais preenchido.

Para melhorar esse teste, deveríamos ter um response para cada request, e deveríamos estar afirmando toda vez que enviarmos uma mensagem. A maneira como estamos fazendo isso agora é armazenando todos os requests e o [[TOK_56]]] final.

Fiz um diagrama para mostrar como ficaria.

https://gyazo.com/dfd4e9f87ff69159f02a0bcc70ae1edc

Acredito fortemente que essa forma é muito melhor no geral para a integridade do teste, já que você está testando praticamente todos os comportamentos do fluxo, em vez de apenas testar a resposta final.


Bem, isso é tudo neste guia, lembre-se que este guia pretende ser uma continuação do guia de casos únicos, se você se sentir perdido, verifique aquele guia que é mais longo e tem mais explicações para tudo.

Lembre-se de que todo o código está armazenado em meu github em this repositório.

Tenha um bom dia!