Интеграционный тест с использованием Bot Framework и DirectLine для потоковых случаев.

· 6 мин чтения

Введение

В предыдущих сообщениях блога мы провели интеграционный тест для отдельных случаев. «Единичные случаи» — это те случаи, что задав что-то боту, он ответит только один раз, а затем мы сравниваем результаты.

В этом руководстве не объясняется, как выполняются аутентификация и вызовы API. Если вы хотите это проверить, ознакомьтесь с руководством для отдельных случаев.

А как насчет плавных разговоров?

Используя текущий способ, у нас нет какого-либо потока в разговоре, например, если вы хотите попросить о помощи, а затем появляется меню с различными опциями, а затем, после выбора одного из них, другое меню. Это составит примерно 2 или более responses. Это означает, что использование решения для интеграционного тестирования, которое мы использовали ранее, не будет работать.

Вот схема того, как работает интеграционный тест для отдельных случаев.

https://gyazo.com/8915f2653033c1143947ef59196403f4

А вот схема того, как мы собираемся адаптировать текущее решение для интеграционного тестирования к потоковым сценариям.

[[[[ТОК_3]]])

Следующее объяснение не охватывает всю основную информацию о том, как работает Bot Framework. Если вы не понимаете, пожалуйста, просмотрите официальную документацию.

Пример случая

В следующем руководстве я буду использовать бота с созданным мной потоковым диалогом, который просит о помощи, а затем выбирает различные варианты.

[[[[ТОК_5]]])

Новая структура JSON

Теперь, когда у нас есть более одного request при разговоре с ботом, нам нужно изменить нашу структуру json, чтобы добавить все request, которые мы будем делать.

[[[ТОК_9]]]

Как видите, произошло важное изменение: request теперь называется requests. Это означает, что теперь у нас есть List<Activity> вместо одного activity.

Новые объекты

Раньше у нас были объекты, настроенные для отдельных случаев: TestEntry и TestEntryCollection. Для случаев потока мы будем создавать новые объекты: TestEntryFlow и TestEntryFlowCollection.

TestEntryFlow

Этот объект предназначен для каждой записи, которая есть в коллекции. Обратите внимание, что объект Requests теперь является List<Activity> вместо одного Activity, как я упоминал ранее.

Поскольку мы будем спрашивать бота несколько раз, нам нужно иметь несколько activities, которые будут отправлены в беседу.

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

Этот объект будет содержать соответствующую информацию для DirectLine, такую как secret и конечные точки, а также список Entries, который мы будем тестировать.

Обратите внимание, что Entries теперь представляет собой список TestEntryFlow, а не 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; }
}

Создание TestMethod для случаев потока

Новый поток

Прежде всего, еще раз взгляните на схему (она та же, что я выложил выше).

https://gyazo.com/ef77fa168d3b5f8b4a116ff38b5edd83

Как видите, структура потока выполнения теста практически такая же:

  1. Получить информацию

  2. Аутентификация

  3. Создать разговорИ вот что изменилось: теперь нам нужно несколько раз отправлять весь запрос в беседу. Для этого нам нужно выполнить цикл для каждого запроса, отправить его боту, а затем сравнить последний ответ с ожидаемым ответом.

  4. Отправьте все запросы

  5. Получить все сообщения

  6. Получите последний ответ

  7. Сравните с ожидаемым ответом

  8. Подтвердить результат

Код

Прежде всего, нам нужно получить информацию из файла, это то же самое, что мы делали раньше с отдельными случаями.

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

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

Теперь нам нужно выполнить цикл для каждого TestEntryFlow из data.entries, при этом мы можем следовать тому же потоку, который мы делали в отдельных случаях, до новой части, где мы зацикливаемся на 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";

Следующий шаг довольно прост: нам нужно зациклить entry.requests и отправить каждый activity в диалог.

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

Мы используем watermark для получения последнего сообщения. watermark — это значение, которое API DirectLine возвращает при запросе информации о разговоре.

После этого нам останется только заполнить globals нашими latestReponse и expectedResponse.

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

И чтобы закончить дело, мы оцениваем строку assert в entry.

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

Окончательный код

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

Улучшения

Я верю, что лучший поток возможен, но это улучшение будет означать, что структуру JSON также следует изменить. Кроме того, чтобы это произошло, json должен быть более заполнен.

Чтобы сделать это тестирование лучше, у нас должен быть response для каждого request, и мы должны утверждать каждый раз, когда отправляем сообщение. Сейчас мы делаем это, сохраняя все requests и финальный response .

Я сделал схему, чтобы показать, как это будет выглядеть.

https://gyazo.com/dfd4e9f87ff69159f02a0bcc70ae1edc

Я твердо убежден, что этот способ в целом намного лучше с точки зрения целостности теста, поскольку вы тестируете практически каждое поведение в потоке, а не просто тестируете окончательный ответ.


Ну, это все, что касается этого руководства. Помните, что это руководство является продолжением руководства для отдельных случаев. Если вы чувствуете себя потерянным, обратитесь к тому руководству, которое длиннее и содержит больше объяснений для всего.

Помните, что весь код хранится у меня на github в репозитории this.

Хорошего дня!