Så här samlar du testrobotar

GÄLLER FÖR: SDK v4

I det här avsnittet visar vi hur du:

  • Skapa enhetstester för robotar.
  • Använd assert för att söka efter aktiviteter som returneras av en dialogruta mot förväntade värden.
  • Använd assert för att kontrollera resultaten som returneras av en dialogruta.
  • Skapa olika typer av datadrivna tester.
  • Skapa falska objekt för de olika beroendena i en dialogruta, till exempel språkidentkännare och så vidare.

Förutsättningar

CoreBot-testexemplet som används i det här avsnittet refererar till paketet Microsoft.Bot.Builder.Testing, XUnit och Moq för att skapa enhetstester.

Huvudrobotexemplet använder Language Understanding (LUIS) för att identifiera användarinsikter. Det är dock inte fokus för den här artikeln att identifiera användar avsikt. Information om hur du identifierar användarsyften finns i Förstå naturligt språk och Lägg till förståelse för naturligt språk i din robot.

Kommentar

Language Understanding (LUIS) dras tillbaka den 1 oktober 2025. Från och med den 1 april 2023 kan du inte skapa nya LUIS-resurser. En nyare version av språktolkning är nu tillgänglig som en del av Azure AI Language.

Conversational Language Understanding (CLU), en funktion i Azure AI Language, är den uppdaterade versionen av LUIS. Mer information om stöd för språktolkning i Bot Framework SDK finns i Förstå naturligt språk.

Testdialogrutor

I CoreBot-exemplet testas dialogrutorna genom DialogTestClient klassen, vilket ger en mekanism för att testa dem isolerat utanför en robot och utan att behöva distribuera koden till en webbtjänst.

Med den här klassen kan du skriva enhetstester som validerar dialogrutors svar tur för tur. Enhetstester som använder DialogTestClient klassen bör fungera med andra dialogrutor som skapats med hjälp av biblioteket för robotbyggaredialogrutor.

I följande exempel visas tester som härletts från DialogTestClient:

var sut = new BookingDialog();
var testClient = new DialogTestClient(Channels.Msteams, sut);

var reply = await testClient.SendActivityAsync<IMessageActivity>("hi");
Assert.Equal("Where would you like to travel to?", reply.Text);

reply = await testClient.SendActivityAsync<IMessageActivity>("Seattle");
Assert.Equal("Where are you traveling from?", reply.Text);

reply = await testClient.SendActivityAsync<IMessageActivity>("New York");
Assert.Equal("When would you like to travel?", reply.Text);

reply = await testClient.SendActivityAsync<IMessageActivity>("tomorrow");
Assert.Equal("OK, I will book a flight from Seattle to New York for tomorrow, Is this Correct?", reply.Text);

reply = await testClient.SendActivityAsync<IMessageActivity>("yes");
Assert.Equal("Sure thing, wait while I finalize your reservation...", reply.Text);

reply = testClient.GetNextReply<IMessageActivity>();
Assert.Equal("All set, I have booked your flight to Seattle for tomorrow", reply.Text);

Klassen DialogTestClient definieras i Microsoft.Bot.Builder.Testing namnområdet och ingår i NuGet-paketet Microsoft.Bot.Builder.Testing .

DialogTestClient

Den första parametern DialogTestClient för är målkanalen. På så sätt kan du testa olika renderingslogik baserat på målkanalen för din robot (Teams, Slack och så vidare). Om du är osäker på målkanalen kan du använda Emulator kanal-ID:n eller Test men tänk på att vissa komponenter kan bete sig annorlunda beroende på den aktuella kanalen, ConfirmPrompt till exempel återger alternativen Ja/Nej på olika sätt för kanalerna Test och Emulator . Du kan också använda den här parametern för att testa logik för villkorsstyrd återgivning i dialogrutan baserat på kanal-ID:t.

Den andra parametern är en instans av dialogrutan som testas. I exempelkoden i den här artikeln sut representerar det system som testas.

Konstruktorn DialogTestClient innehåller ytterligare parametrar som gör att du kan anpassa klientbeteendet ytterligare eller skicka parametrar till den dialogruta som testas om det behövs. Du kan skicka initieringsdata för dialogrutan, lägga till anpassade mellanprogram eller använda din egen TestAdapter och ConversationState instans.

Skicka och ta emot meddelanden

Med SendActivityAsync<IActivity> metoden kan du skicka ett textyttrande eller ett IActivity till din dialogruta och returnerar det första meddelandet som det tar emot. Parametern <T> används för att returnera en stark typinstans av svaret så att du kan hävda det utan att behöva casta det.

var reply = await testClient.SendActivityAsync<IMessageActivity>("hi");
Assert.Equal("Where would you like to travel to?", reply.Text);

I vissa scenarier kan din robot skicka flera meddelanden som svar på en enda aktivitet. I dessa fall DialogTestClient placeras svaren i kö och du kan använda GetNextReply<IActivity> metoden för att visa nästa meddelande från svarskön.

reply = testClient.GetNextReply<IMessageActivity>();
Assert.Equal("All set, I have booked your flight to Seattle for tomorrow", reply.Text);

GetNextReply<IActivity> returnerar null om det inte finns några ytterligare meddelanden i svarskön.

Bekräfta aktiviteter

Koden i CoreBot-exemplet anger endast egenskapen för Text de returnerade aktiviteterna. I mer komplexa robotar kanske du vill hävda andra egenskaper som Speak, InputHint, ChannelDataoch så vidare.

Assert.Equal("Sure thing, wait while I finalize your reservation...", reply.Text);
Assert.Equal("One moment please...", reply.Speak);
Assert.Equal(InputHints.IgnoringInput, reply.InputHint);

Du kan göra detta genom att kontrollera varje egenskap individuellt enligt ovan, du kan skriva egna hjälpverktyg för att kontrollera aktiviteter eller använda andra ramverk som FluentAssertions för att skriva anpassade intyg och förenkla testkoden.

Skicka parametrar till dina dialogrutor

Konstruktorn DialogTestClient har en initialDialogOptions som kan användas för att skicka parametrar till din dialogruta. I det här exemplet initierar till exempel MainDialog ett BookingDetails objekt från språkigenkänningsresultatet med de entiteter som löses från användarens yttrande och skickar det här objektet i anropet för att anropa BookingDialog.

Du kan implementera detta i ett test på följande sätt:

var inputDialogParams = new BookingDetails()
{
    Destination = "Seattle",
    TravelDate = $"{DateTime.UtcNow.AddDays(1):yyyy-MM-dd}"
};

var sut = new BookingDialog();
var testClient = new DialogTestClient(Channels.Msteams, sut, inputDialogParams);

BookingDialog tar emot den här parametern och kommer åt den i testet på samma sätt som den skulle ha varit när den anropades från MainDialog.

private async Task<DialogTurnResult> DestinationStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    var bookingDetails = (BookingDetails)stepContext.Options;
    ...
}

Bekräfta dialogruteresultat

Vissa dialogrutor som BookingDialog eller DateResolverDialog returnerar ett värde i den anropande dialogrutan. Objektet DialogTestClient exponerar en DialogTurnResult egenskap som kan användas för att analysera och kontrollera de resultat som returneras av dialogrutan.

Till exempel:

var sut = new BookingDialog();
var testClient = new DialogTestClient(Channels.Msteams, sut);

var reply = await testClient.SendActivityAsync<IMessageActivity>("hi");
Assert.Equal("Where would you like to travel to?", reply.Text);

...

var bookingResults = (BookingDetails)testClient.DialogTurnResult.Result;
Assert.Equal("New York", bookingResults?.Origin);
Assert.Equal("Seattle", bookingResults?.Destination);
Assert.Equal("2019-06-21", bookingResults?.TravelDate);

Egenskapen DialogTurnResult kan också användas för att inspektera och kontrollera mellanliggande resultat som returneras av stegen i ett vattenfall.

Analysera testutdata

Ibland är det nödvändigt att läsa en enhetstestavskrift för att analysera testkörningen utan att behöva felsöka testet.

Paketet Microsoft.Bot.Builder.Testing innehåller en XUnitDialogTestLogger som loggar de meddelanden som skickas och tas emot av dialogrutan till konsolen.

Om du vill använda det här mellanprogrammet måste testet exponera en konstruktor som tar emot ett ITestOutputHelper objekt som tillhandahålls av XUnit-testköraren och skapar ett XUnitDialogTestLogger som skickas via DialogTestClient parametern middlewares .

public class BookingDialogTests
{
    private readonly IMiddleware[] _middlewares;

    public BookingDialogTests(ITestOutputHelper output)
        : base(output)
    {
        _middlewares = new[] { new XUnitDialogTestLogger(output) };
    }

    [Fact]
    public async Task SomeBookingDialogTest()
    {
        // Arrange
        var sut = new BookingDialog();
        var testClient = new DialogTestClient(Channels.Msteams, sut, middlewares: _middlewares);

        ...
    }
}

Här är ett exempel på vad loggarna XUnitDialogTestLogger till utdatafönstret när det har konfigurerats:

Example middleware output from XUnit.

Mer information om hur du skickar testutdata till konsolen när du använder XUnit finns i Samla in utdata i XUnit-dokumentationen.

Dessa utdata loggas också på byggservern under de kontinuerliga integreringsversionerna och hjälper dig att analysera byggfel.

Datadrivna tester

I de flesta fall ändras inte dialoglogik och de olika körningsvägarna i en konversation baseras på användaryttranden. I stället för att skriva ett enskilt enhetstest för varje variant i konversationen är det enklare att använda datadrivna tester (även kallat parametriserat test).

Exempeltestet i översiktsavsnittet i det här dokumentet visar till exempel hur du testar ett körningsflöde, men inte andra, till exempel:

  • Vad händer om användaren säger nej till bekräftelsen?
  • Vad händer om de använder ett annat datum?

Med datadrivna tester kan vi testa alla dessa permutationer utan att behöva skriva om testerna.

I CoreBot-exemplet använder Theory vi tester från XUnit för att parametrisera tester.

Teoritester med InlineData

Följande test kontrollerar att en dialogruta avbryts när användaren säger "avbryt".

[Fact]
public async Task ShouldBeAbleToCancel()
{
    var sut = new TestCancelAndHelpDialog();
    var testClient = new DialogTestClient(Channels.Test, sut);

    var reply = await testClient.SendActivityAsync<IMessageActivity>("Hi");
    Assert.Equal("Hi there", reply.Text);
    Assert.Equal(DialogTurnStatus.Waiting, testClient.DialogTurnResult.Status);

    reply = await testClient.SendActivityAsync<IMessageActivity>("cancel");
    Assert.Equal("Cancelling...", reply.Text);
}

Om du vill avbryta en dialogruta kan användarna skriva "quit", "never mind" och "stop it". I stället för att skriva ett nytt testfall för varje möjligt ord skriver du en enskild Theory testmetod som accepterar parametrar via en lista med InlineData värden för att definiera parametrarna för varje testfall:

[Theory]
[InlineData("cancel")]
[InlineData("quit")]
[InlineData("never mind")]
[InlineData("stop it")]
public async Task ShouldBeAbleToCancel(string cancelUtterance)
{
    var sut = new TestCancelAndHelpDialog();
    var testClient = new DialogTestClient(Channels.Test, sut, middlewares: _middlewares);

    var reply = await testClient.SendActivityAsync<IMessageActivity>("Hi");
    Assert.Equal("Hi there", reply.Text);
    Assert.Equal(DialogTurnStatus.Waiting, testClient.DialogTurnResult.Status);

    reply = await testClient.SendActivityAsync<IMessageActivity>(cancelUtterance);
    Assert.Equal("Cancelling...", reply.Text);
}

Det nya testet körs fyra gånger med de olika parametrarna och varje ärende visas som ett underordnat ShouldBeAbleToCancel objekt under testet i Visual Studio Test Explorer. Om någon av dem misslyckas enligt nedan kan du högerklicka och felsöka scenariot som misslyckades i stället för att köra hela testuppsättningen igen.

Example test results for in-line data.

Teoritester med hjälp av MemberData och komplexa typer

InlineData är användbart för små datadrivna tester som tar emot enkla värdetypsparametrar (sträng, int och så vidare).

Tar BookingDialog emot ett BookingDetails objekt och returnerar ett nytt BookingDetails objekt. En icke-parametriserad version av ett test för den här dialogrutan skulle se ut så här:

[Fact]
public async Task DialogFlow()
{
    // Initial parameters
    var initialBookingDetails = new BookingDetails
    {
        Origin = "Seattle",
        Destination = null,
        TravelDate = null,
    };

    // Expected booking details
    var expectedBookingDetails = new BookingDetails
    {
        Origin = "Seattle",
        Destination = "New York",
        TravelDate = "2019-06-25",
    };

    var sut = new BookingDialog();
    var testClient = new DialogTestClient(Channels.Test, sut, initialBookingDetails);

    // Act/Assert
    var reply = await testClient.SendActivityAsync<IMessageActivity>("hi");
    ...

    var bookingResults = (BookingDetails)testClient.DialogTurnResult.Result;
    Assert.Equal(expectedBookingDetails.Origin, bookingResults?.Origin);
    Assert.Equal(expectedBookingDetails.Destination, bookingResults?.Destination);
    Assert.Equal(expectedBookingDetails.TravelDate, bookingResults?.TravelDate);
}

För att parametrisera det här testet skapade vi en BookingDialogTestCase klass som innehåller våra testfallsdata. Det innehåller det första BookingDetails objektet, det förväntade BookingDetails och en matris med strängar som innehåller de yttranden som skickas från användaren och förväntade svar från dialogrutan för varje tur.

public class BookingDialogTestCase
{
    public BookingDetails InitialBookingDetails { get; set; }

    public string[,] UtterancesAndReplies { get; set; }

    public BookingDetails ExpectedBookingDetails { get; set; }
}

Vi har också skapat en hjälpklass BookingDialogTestsDataGenerator som exponerar en IEnumerable<object[]> BookingFlows() metod som returnerar en samling testfall som ska användas av testet.

För att kunna visa varje testfall som ett separat objekt i Visual Studio Test Explorer kräver XUnit-testköraren att komplexa typer som BookingDialogTestCase implementerar IXunitSerializable, för att förenkla detta, tillhandahåller Bot.Builder.Testing-ramverket en TestDataObject klass som implementerar det här gränssnittet och kan användas för att omsluta testfallsdata utan att behöva implementera IXunitSerializable.

Här är ett fragment av IEnumerable<object[]> BookingFlows() som visar hur de två klasserna används:

public static class BookingDialogTestsDataGenerator
{
    public static IEnumerable<object[]> BookingFlows()
    {
        // Create the first test case object
        var testCaseData = new BookingDialogTestCase
        {
            InitialBookingDetails = new BookingDetails(),
            UtterancesAndReplies = new[,]
            {
                { "hi", "Where would you like to travel to?" },
                { "Seattle", "Where are you traveling from?" },
                { "New York", "When would you like to travel?" },
                { "tomorrow", $"Please confirm, I have you traveling to: Seattle from: New York on: {DateTime.Now.AddDays(1):yyyy-MM-dd}. Is this correct? (1) Yes or (2) No" },
                { "yes", null },
            },
            ExpectedBookingDetails = new BookingDetails
            {
                Destination = "Seattle",
                Origin = "New York",
                TravelDate = $"{DateTime.Now.AddDays(1):yyyy-MM-dd}",
            }, 
        };
        // wrap the test case object into TestDataObject and return it.
        yield return new object[] { new TestDataObject(testCaseData) };

        // Create the second test case object
        testCaseData = new BookingDialogTestCase
        {
            InitialBookingDetails = new BookingDetails
            {
                Destination = "Seattle",
                Origin = "New York",
                TravelDate = null,
            },
            UtterancesAndReplies = new[,]
            {
                { "hi", "When would you like to travel?" },
                { "tomorrow", $"Please confirm, I have you traveling to: Seattle from: New York on: {DateTime.Now.AddDays(1):yyyy-MM-dd}. Is this correct? (1) Yes or (2) No" },
                { "yes", null },
            },
            ExpectedBookingDetails = new BookingDetails
            {
                Destination = "Seattle",
                Origin = "New York",
                TravelDate = $"{DateTime.Now.AddDays(1):yyyy-MM-dd}",
            },
        };
        // wrap the test case object into TestDataObject and return it.
        yield return new object[] { new TestDataObject(testCaseData) };
    }
}

När vi har skapat ett objekt för att lagra testdata och en klass som exponerar en samling testfall använder vi XUnit-attributet MemberData i stället för InlineData för att mata in data i testet. Den första parametern för MemberData är namnet på den statiska funktion som returnerar samlingen av testfall och den andra parametern är den typ av klass som exponerar den här metoden.

[Theory]
[MemberData(nameof(BookingDialogTestsDataGenerator.BookingFlows), MemberType = typeof(BookingDialogTestsDataGenerator))]
public async Task DialogFlowUseCases(TestDataObject testData)
{
    // Get the test data instance from TestDataObject
    var bookingTestData = testData.GetObject<BookingDialogTestCase>();
    var sut = new BookingDialog();
    var testClient = new DialogTestClient(Channels.Test, sut, bookingTestData.InitialBookingDetails);

    // Iterate over the utterances and replies array.
    for (var i = 0; i < bookingTestData.UtterancesAndReplies.GetLength(0); i++)
    {
        var reply = await testClient.SendActivityAsync<IMessageActivity>(bookingTestData.UtterancesAndReplies[i, 0]);
        Assert.Equal(bookingTestData.UtterancesAndReplies[i, 1], reply?.Text);
    }

    // Assert the resulting BookingDetails object
    var bookingResults = (BookingDetails)testClient.DialogTurnResult.Result;
    Assert.Equal(bookingTestData.ExpectedBookingDetails?.Origin, bookingResults?.Origin);
    Assert.Equal(bookingTestData.ExpectedBookingDetails?.Destination, bookingResults?.Destination);
    Assert.Equal(bookingTestData.ExpectedBookingDetails?.TravelDate, bookingResults?.TravelDate);
}

Här är ett exempel på resultaten för testerna DialogFlowUseCases i Visual Studio Test Explorer när testet körs:

Example results for the booking dialog.

Använda mocks

Du kan använda falska element för de saker som för närvarande inte testas. Som referens kan den här nivån vanligtvis betraktas som enhets- och integreringstestning.

Genom att håna så många element som möjligt kan du få bättre isolering av det stycke som du testar. Kandidater för falska element är lagring, adapter, mellanprogram, aktivitetspipeline, kanaler och allt annat som inte är direkt en del av roboten. Detta kan också innebära att ta bort vissa aspekter tillfälligt, till exempel mellanprogram som inte ingår i den del av roboten som du testar, för att isolera varje del. Men om du testar mellanprogrammet kanske du vill håna roboten i stället.

Att håna element kan ta en handfull former, från att ersätta ett element med ett annat känt objekt till att implementera minimala hello world-funktioner. Detta kan också ske i form av att ta bort elementet, om det inte är nödvändigt, eller tvinga det att inte göra någonting.

Med modeller kan vi konfigurera beroenden för en dialogruta och se till att de är i ett känt tillstånd under körningen av testet utan att behöva förlita sig på externa resurser som databaser, språkmodeller eller andra objekt.

För att göra dialogrutan enklare att testa och minska dess beroenden för externa objekt kan du behöva mata in de externa beroendena i dialogkonstruktorn.

I stället för att till exempel instansiera BookingDialog i MainDialog:

public MainDialog()
    : base(nameof(MainDialog))
{
    ...
    AddDialog(new BookingDialog());
    ...
}

Vi skickar en instans av BookingDialog som en konstruktorparameter:

public MainDialog(BookingDialog bookingDialog)
    : base(nameof(MainDialog))
{
    ...
    AddDialog(bookingDialog);
    ...
}

På så sätt kan vi ersätta instansen BookingDialog med ett modellobjekt och skriva enhetstester utan MainDialog att behöva anropa den faktiska BookingDialog klassen.

// Create the mock object
var mockDialog = new Mock<BookingDialog>();

// Use the mock object to instantiate MainDialog
var sut = new MainDialog(mockDialog.Object);

var testClient = new DialogTestClient(Channels.Test, sut);

Dialogrutor för att håna

Enligt beskrivningen ovan MainDialog anropas BookingDialog för att hämta objektet BookingDetails . Vi implementerar och konfigurerar en falsk instans av BookingDialog på följande sätt:

// Create the mock object for BookingDialog.
var mockDialog = new Mock<BookingDialog>();
mockDialog
    .Setup(x => x.BeginDialogAsync(It.IsAny<DialogContext>(), It.IsAny<object>(), It.IsAny<CancellationToken>()))
    .Returns(async (DialogContext dialogContext, object options, CancellationToken cancellationToken) =>
    {
        // Send a generic activity so we can assert that the dialog was invoked.
        await dialogContext.Context.SendActivityAsync($"{mockDialogNameTypeName} mock invoked", cancellationToken: cancellationToken);

        // Create the BookingDetails instance we want the mock object to return.
        var expectedBookingDialogResult = new BookingDetails()
        {
            Destination = "Seattle",
            Origin = "New York",
            TravelDate = $"{DateTime.UtcNow.AddDays(1):yyyy-MM-dd}"
        };

        // Return the BookingDetails we need without executing the dialog logic.
        return await dialogContext.EndDialogAsync(expectedBookingDialogResult, cancellationToken);
    });

// Create the sut (System Under Test) using the mock booking dialog.
var sut = new MainDialog(mockDialog.Object);

I det här exemplet använde vi Moq för att skapa den falska dialogrutan och Setup metoderna och Returns för att konfigurera dess beteende.

Håna LUIS-resultat

Kommentar

Language Understanding (LUIS) dras tillbaka den 1 oktober 2025. Från och med den 1 april 2023 kan du inte skapa nya LUIS-resurser. En nyare version av språktolkning är nu tillgänglig som en del av Azure AI Language.

Conversational Language Understanding (CLU), en funktion i Azure AI Language, är den uppdaterade versionen av LUIS. Mer information om stöd för språktolkning i Bot Framework SDK finns i Förstå naturligt språk.

I enkla scenarier kan du implementera falska LUIS-resultat via kod på följande sätt:

var mockRecognizer = new Mock<IRecognizer>();
mockRecognizer
    .Setup(x => x.RecognizeAsync<FlightBooking>(It.IsAny<ITurnContext>(), It.IsAny<CancellationToken>()))
    .Returns(() =>
    {
        var luisResult = new FlightBooking
        {
            Intents = new Dictionary<FlightBooking.Intent, IntentScore>
            {
                { FlightBooking.Intent.BookFlight, new IntentScore() { Score = 1 } },
            },
            Entities = new FlightBooking._Entities(),
        };
        return Task.FromResult(luisResult);
    });

LUIS-resultat kan vara komplexa. När de är det är det enklare att samla in önskat resultat i en JSON-fil, lägga till den som en resurs i projektet och deserialisera den till ett LUIS-resultat. Här är ett exempel:

var mockRecognizer = new Mock<IRecognizer>();
mockRecognizer
    .Setup(x => x.RecognizeAsync<FlightBooking>(It.IsAny<ITurnContext>(), It.IsAny<CancellationToken>()))
    .Returns(() =>
    {
        // Deserialize the LUIS result from embedded json file in the TestData folder.
        var bookingResult = GetEmbeddedTestData($"{GetType().Namespace}.TestData.FlightToMadrid.json");

        // Return the deserialized LUIS result.
        return Task.FromResult(bookingResult);
    });

Ytterligare information