اختبار التكامل باستخدام Bot Framework وDirectLine لحالات التدفق

· 7 دقيقة قراءة

مقدمة

في منشورات المدونة السابقة، أجرينا بعض اختبارات التكامل لحالات فردية. “الحالات الفردية” هي تلك الحالات التي بعد سؤال الروبوت عن شيء ما، سوف يرد مرة واحدة فقط ثم نقوم بمقارنة النتائج.

لن يشرح هذا الدليل كيفية إجراء المصادقة واستدعاءات واجهة برمجة التطبيقات (API)، إذا كنت تريد التحقق من ذلك، فيرجى مراجعة دليل الحالات الفردية.

ماذا عن محادثات التدفق؟

باستخدام الطريقة الحالية، ليس لدينا أي نوع من التدفق في المحادثة، على سبيل المثال، إذا كنت تريد طلب المساعدة ثم تظهر قائمة بخيارات مختلفة، ثم بعد تحديد أحدها، تظهر قائمة أخرى. وهذا من شأنه أن يجعل مثل 2 أو أكثر responses. وهذا يعني أن استخدام حل اختبار التكامل الذي استخدمناه من قبل، لن ينجح.

فيما يلي رسم تخطيطي لكيفية عمل اختبار التكامل للحالات الفردية.

!https://gyazo.com/8915f2653033c1143947ef59196403f4](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. وهذا يعني أن لدينا الآن 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](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 هي القيمة التي ترجعها واجهة برمجة تطبيقات 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](https://gyazo.com/dfd4e9f87ff69159f02a0bcc70ae1edc)

أعتقد بشدة أن هذه الطريقة أفضل بكثير بشكل عام لسلامة الاختبار، نظرًا لأنك تختبر تقريبًا كل سلوك في التدفق بدلاً من مجرد اختبار الاستجابة النهائية.


حسنًا، هذا كل ما في هذا الدليل، يرجى تذكر أن هذا الدليل يهدف إلى أن يكون استمرارًا لدليل الحالات الفردية، إذا شعرت بالضياع، فراجع هذا الدليل الأطول والذي يحتوي على المزيد من الشرح لكل شيء.

تذكر أنه تم تخزين كافة التعليمات البرمجية في جيثب الخاص بي في مستودع هذا.

اتمنى لك يوم جيد!