我如何在没有对话框的情况下对我的机器人进行单元测试? (C#)

How can i unit test my bot without dialogs? (c#)

我目前正在 Microsoft Bot Framework 4 中进行自动化单元测试。 从那里,我想检查机器人的简单对话语句。在核心机器人中 测试样本 (https://docs.microsoft.com/en-us/azure/bot-service/unit-test-bots?view=azure-bot-service-4.0&tabs=csharp) 演示了如何做到这一点,但对我来说,我的机器人没有使用对话框(据我所知)。

我现在的问题是,如何对简单的 Question/Answer 语句进行单元测试? 我的主要目标是对我的 QnA Maker 知识库进行单元测试。

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Schema;
using Microsoft.Extensions.Logging;
using System;
using Newtonsoft.Json;
using System.IO;
using Microsoft.Bot.Builder.AI.QnA;

using Microsoft.Extensions.Configuration;

namespace SEKAI.Bots
{
    public class DispatchBot : ActivityHandler
    {
        private ILogger<DispatchBot> _logger;
        private IBotServices _botServices;
        private IConfiguration _configuration;

        public DispatchBot(IConfiguration configuration, IBotServices botServices, ILogger<DispatchBot> logger)
        {
            _configuration = configuration;
            _logger = logger;
            _botServices = botServices;
        }

        protected async Task NoMatchFound(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
        {
            var table = BotUtility.BotUtility.GetTableReference(_configuration);
            BotUtility.BotUtility.InsertRecord(turnContext, table);

            // Wird ausgeführt, wenn keine KnowledgeBase gefunden wird
            System.Diagnostics.Debug.WriteLine("### FINDE KEINE PASSENDE ANTWORT ###");
            await turnContext.SendActivityAsync(MessageFactory.Text("Leider kann ich Ihnen hierbei noch nicht weiterhelfen. Ich bin aber schon dabei, Neues zu lernen!"), cancellationToken);
        }

        // Wenn der Benutzer den Chat startet
        protected override async Task OnEventActivityAsync(ITurnContext<IEventActivity> turnContext, CancellationToken cancellationToken)
        {
            if (turnContext.Activity.Name == "webchat/join")
            {
                string WelcomeCardpath = Path.Combine(".", "AdaptiveCards", "WelcomeCard.json");
                var cardAttachment = BotUtility.BotUtility.CreateAdaptiveCard(WelcomeCardpath);
                await turnContext.SendActivityAsync(MessageFactory.Attachment(cardAttachment), cancellationToken);
            }
        }

        // Wenn ein Nutzer eine Nachricht schreibt
        protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
        {
            // First, we use the dispatch model to determine which cognitive service (LUIS or QnA) to use.
            var recognizerResult = await _botServices.Dispatch.RecognizeAsync(turnContext, cancellationToken);

            // Top intent tell us which cognitive service to use.
            var topIntent = recognizerResult.GetTopScoringIntent();

            // Next, we call the dispatcher with the top intent.
            await DispatchToTopIntentAsync(turnContext, topIntent.intent, recognizerResult, cancellationToken);
        }

        // Suche nach der richtigen KnowledgeBase
        private async Task DispatchToTopIntentAsync(ITurnContext<IMessageActivity> turnContext, string intent, RecognizerResult recognizerResult, CancellationToken cancellationToken)
        {
            switch (intent)
            {
                case "q_SEKAI_Chitchat":
                    await ProcessSEKAI_ChitchatAsync(turnContext, cancellationToken);
                    break;

                default:
                    // Wird ausgeführt, wenn keine KnowledgeBase gefunden wird
                    _logger.LogInformation($"Dispatch unrecognized intent: {intent}.");
                    await NoMatchFound(turnContext, cancellationToken);
                    break;
            }
        }

        // Bearbeitung aus SEKAI_Chitchat
        private async Task ProcessSEKAI_ChitchatAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
        {
            _logger.LogInformation("ProcessSEKAI_ChitchatAsync");

            // Confidence Score zur KnowledgeBase
            var metadata = new Metadata();
            var qnaOptions = new QnAMakerOptions();
            qnaOptions.ScoreThreshold = 0.70F;

            // Speichere die Antwort aus der KnowledgeBase
            var results = await _botServices.SEKAI_Chitchat.GetAnswersAsync(turnContext, qnaOptions);
            if (results.Any())
            {
                // Speichere die Antwort aus der KnowledgeBase für die Schlüsselwort-Verarbeitung
                string savetext = results.First().Answer;
                System.Diagnostics.Debug.WriteLine(savetext);

                if (savetext.Contains("{card}"))
                {
                    // Hier wird das Schlüsselwort für die Antwortausgabe entfernt
                    int firstKeyword = savetext.IndexOf("{card}") + "{card}".Length;
                    int lastKeyword = savetext.LastIndexOf("{/card}");
                    string subsavetext = savetext.Substring(firstKeyword, lastKeyword - firstKeyword);
                    System.Diagnostics.Debug.WriteLine(subsavetext);

                    // Ausgabe der Adaptive Card
                    savetext = savetext.Replace("{card}" + subsavetext + "{/card}", "");
                    string AdaptiveCardPath = Path.Combine(".", "AdaptiveCards", subsavetext + ".json");
                    var cardAttachment = BotUtility.BotUtility.CreateAdaptiveCard(AdaptiveCardPath);
                    await turnContext.SendActivityAsync(MessageFactory.Attachment(cardAttachment), cancellationToken);

                    // Ausgabe von Text
                    await turnContext.SendActivityAsync(MessageFactory.Text(savetext), cancellationToken);
                }
                else
                {
                    // Befindet sich in der Datenbank kein Schlüsselwort, wird nur die Antwort ausgegeben
                    await turnContext.SendActivityAsync(MessageFactory.Text(savetext), cancellationToken);
                }
            }
            else
            {
                // Wird ausgegeben, wenn keine Antwort in der KnowledgeBase passt
                await NoMatchFound(turnContext, cancellationToken);
            }
        }
    }
}

我构建的机器人完全基于 NLP with Dispatch 示例 (https://github.com/microsoft/BotBuilder-Samples/tree/main/samples/csharp_dotnetcore/14.nlp-with-dispatch)。

我已经对机器人做了一些修改,这就是为什么我为我的版本添加了主文件(GitHub 存储库中的 Dispatchbot.cs 文件)。

我无法帮助您了解 C# 的语法,但我在我的 nodejs 机器人中进行了一些非对话框测试,这可能对您有所帮助。本质上,您只需创建一个 TestAdapter 并将其与 activity 一起传递给您的机器人。例如,这是我的 dispatchBot.test.js 文件的一部分:

const { TestAdapter, ActivityTypes, TurnContext, ConversationState, MemoryStorage, UserState } = require('botbuilder');
const { DispatchBot } = require('../../bots/dispatchBot');
const assert = require('assert');
const nock = require('nock');
require('dotenv').config({ path: './.env' });

// Tests using mocha framework
describe('Dispatch Bot Tests', () => {

    const testAdapter = new TestAdapter();
    async function processActivity(activity, bot) {
        const context = new TurnContext(testAdapter, activity);
        await bot.run(context);
    }

    it('QnA Generic Response'), async () => {
        const memoryStorage = new MemoryStorage();
        let bot = new DispatchBot(new ConversationState(memoryStorage), new UserState(memoryStorage), appInsightsClient);

        // Create message activity
        const messageActivity = {
            type: ActivityTypes.Message,
            channelId: 'test',
            conversation: {
                id: 'someId'
            },
            from: { id: 'theUser' },
            recipient: { id: 'theBot' },
            text: `This is an unrecognized QnA utterance for testing`
        };

        // Send the conversation update activity to the bot.
        await processActivity(messageActivity, bot);

        // Assert we got the right response
        let reply = testAdapter.activityBuffer.shift();
        assert.equal(reply.text,defaultQnaResponse);
    });
});

如果您的机器人响应多个活动,您只需继续使用 testAdapter.activityBuffer.shift(),即使只是为了清除缓冲区。否则它会将这些消息保留在缓冲区中以供后续测试使用。希望这会有所帮助!