Search code examples
c#unit-testingbotframeworkbots

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


i am currently working on automated unit tests inside the Microsoft Bot Framework 4. From there, i want to check simple conversational statements from the bot. In the CoreBot Tests sample (https://learn.microsoft.com/en-us/azure/bot-service/unit-test-bots?view=azure-bot-service-4.0&tabs=csharp) is demonstrated how it is possible to do that but for me, my bot isnt using dialogs (as far as i know).

My question here is now, how can i unit test simple Question/Answer Statements? My main goal is to unit test my QnA Maker Knowledge Bases.

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

The bot i have build is based completly on the NLP with Dispatch sample (https://github.com/microsoft/BotBuilder-Samples/tree/main/samples/csharp_dotnetcore/14.nlp-with-dispatch).

I have already modified the bot a bit and thats why i added the main file (Dispatchbot.cs file in the GitHub repo) for my version.


Solution

  • I can't help you with the syntax for C#, but I have some tests in my nodejs bot that are not dialogs and this may help you. Essentially, you just create a TestAdapter and pass that with an activity to your bot. For example, here is part of my dispatchBot.test.js file:

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

    If your bot responds with multiple activities, you just need to continue to use testAdapter.activityBuffer.shift(), even if just to clear the buffer. Otherwise it would keep those messages in the buffer for subsequent tests. Hopefully this will help!