Teste de integração usando Bot Framework e DirectLine para casos de fluxo
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.
E aqui está um diagrama de como vamos adaptar a atual solução de teste de integração aos casos de fluxo.
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.
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).
Como você pode ver, a estrutura do fluxo para fazer o teste é praticamente a mesma:
Obtenha informações
Autenticar
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.
Envie todas as solicitações
Receba todas as mensagens
Obtenha a resposta mais recente
Compare com a resposta esperada
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.
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!



