흐름 사례에 대해 Bot Framework 및 DirectLine을 사용한 통합 테스트

· 6분 읽기

소개

이전 블로그 게시물에서는 단일 사례에 대한 몇 가지 통합 테스트를 수행했습니다. “단일 사례"는 봇에게 무언가를 요청한 후 한 번만 응답하고 결과를 비교하는 경우입니다.

이 가이드에서는 인증 및 API 호출이 어떻게 수행되는지 설명하지 않습니다. 확인하려면 단일 사례 가이드를 확인하세요.

흐름 대화는 어떻습니까?

현재 방식을 사용하면 대화에 어떤 종류의 흐름도 없습니다. 예를 들어, 도움을 요청하려는 경우 메뉴에 다양한 옵션이 표시되고 그 중 하나를 선택한 후 다른 메뉴가 표시됩니다. 이렇게 하면 2개 이상의 responses이 됩니다. 즉, 이전에 사용했던 통합 테스트 솔루션을 사용하면 작동하지 않는다는 의미입니다.

다음은 단일 사례에 대한 통합 테스트가 작동하는 방식을 보여주는 다이어그램입니다.

https://gyazo.com/8915f2653033c1143947ef59196403f4

다음은 현재 통합 테스트 솔루션을 흐름 사례에 어떻게 적용할 것인지에 대한 다이어그램입니다.

https://gyazo.com/ef77fa168d3b5f8b4a116ff38b5edd83

다음 설명에서는 Bot Framework 작동 방식에 대한 모든 기본 정보를 다루지 않습니다. 이해가 되지 않는 경우 공식 문서를 확인하세요.

예시 사례

다음 가이드에서는 도움을 요청하고 다른 옵션을 선택하는 흐름 대화가 포함된 봇을 사용하겠습니다.

https://gyazo.com/0a1104cce67c07331a6e0fbd8e19b3e2

새로운 JSON 구조

이제 봇과 대화할 때 하나 이상의 request가 있으므로 json 구조를 수정하여 수행할 모든 request를 추가해야 합니다.

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

보시다시피 중요한 변경 사항이 있습니다. request는 이제 requests입니다. 이는 이제 단일 activity 대신 List<Activity>가 있음을 의미합니다.

새로운 객체

이전에는 TestEntryTestEntryCollection라는 단일 사례에 대해 개체를 설정했습니다. 흐름 사례의 경우 TestEntryFlowTestEntryFlowCollection라는 새 객체를 생성합니다.

TestEntryFlow

이 객체는 컬렉션에 있는 모든 항목에 대한 것입니다. 앞서 언급한 것처럼 Requests 객체는 이제 단일 Activity 대신 List<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

이 객체에는 secret 및 엔드포인트와 같은 DirectLine에 대한 관련 정보와 테스트할 Entries 목록이 포함됩니다.

Entries는 이제 TestEntry가 아니라 TestEntryFlow의 목록입니다.

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

이제 data.entries의 각 TestEntryFlow에 대해 반복해야 합니다. 이를 통해 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는 DirectLine API가 대화 정보를 요청할 때 반환하는 값입니다.

그런 다음 globalslatestReponseexpectedResponse로 채워야 합니다.

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

그리고 사례를 마무리하기 위해 entry에서 assert 문자열을 평가합니다.

/// 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을 더 많이 채워야 합니다.

이 테스트를 더 좋게 만들기 위해서는 각 request에 대해 response가 있어야 하며 메시지를 보낼 때마다 주장해야 합니다. 지금 우리가 하고 있는 방법은 모든 requests최종 response 을 저장하는 것입니다.

이것이 어떻게 보이는지 보여주기 위해 다이어그램을 만들었습니다.

https://gyazo.com/dfd4e9f87ff69159f02a0bcc70ae1edc

나는 최종 응답을 테스트하는 대신 흐름의 거의 모든 동작을 테스트하기 때문에 이 방법이 테스트의 무결성을 위해 전반적으로 훨씬 더 낫다고 생각합니다.


이 가이드는 이것이 전부입니다. 이 가이드는 단일 사례 가이드의 연속이라는 점을 기억하세요. 길을 잃은 것 같으면 모든 것에 대해 더 길고 더 많은 설명이 포함된 가이드를 확인하세요.

모든 코드는 내 github의 this 저장소에 저장되어 있습니다.

좋은 하루 보내세요!