Durchführen von Komponententests für BotsHow to unit test bots

gilt für: SDK v4APPLIES TO: SDK v4

In diesem Thema wird Folgendes beschrieben:In this topic we'll show you how to:

  • Erstellen von Komponententests für BotsCreate unit tests for bots
  • Verwenden einer Bestätigung für einen Abgleich der Aktivitäten, die von einem Dialog-Turn zurückgegeben werden, mit den erwarteten WertenUse assert to check for activities returned by a dialog turn against expected values
  • Verwenden einer Bestätigung zum Überprüfen der Ergebnisse, die von einem Dialog zurückgegeben werdenUse assert to check the results returned by a dialog
  • Erstellen unterschiedlicher Arten von datengesteuerten TestsCreate different types of data driven tests
  • Erstellen von Pseudoobjekten für die verschiedenen Abhängigkeiten eines Dialogs (z. B. LUIS-Erkennungen usw.)Create mock objects for the different dependencies of a dialog (i.e. LUIS recognizers, etc.)

VoraussetzungenPrerequisites

Im CoreBot.Tests-Beispiel dieses Themas wird zum Erstellen von Komponententests auf das Microsoft.Bot.Builder.Testing-Paket, XUnit und Moq verwiesen.The CoreBot Tests sample used in this topic references the Microsoft.Bot.Builder.Testing package, XUnit, and Moq to create unit tests.

Testen von DialogenTesting Dialogs

Im CoreBot-Beispiel werden die Komponententests für die Dialoge mit der DialogTestClient-Klasse durchgeführt. Diese Klasse verfügt über einen Mechanismus für das isolierte Testen außerhalb eines Bots, ohne dass Ihr Code über einen Webdienst bereitgestellt werden muss.In the CoreBot sample, dialogs are unit tested through the DialogTestClient class which provides a mechanism for testing them in isolation outside of a bot and without having to deploy your code to a web service.

Bei Verwendung dieser Klasse können Sie Komponententests schreiben, mit denen Dialogantworten Turn für Turn überprüft werden.Using this class, you can write unit tests that validate dialogs responses on a turn-by-turn basis. Komponententests mit der DialogTestClient-Klasse sollten auch für andere Dialoge funktionieren, die mit der botbuilder-Dialogbibliothek erstellt werden.Unit tests using DialogTestClient class should work with other dialogs built using the botbuilder dialogs library.

Im folgenden Beispiel werden Tests veranschaulicht, die von DialogTestClient abgeleitet sind:The following example demonstrates tests derived from 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);

Die DialogTestClient-Klasse ist im Microsoft.Bot.Builder.Testing-Namespace definiert und im NuGet-Paket Microsoft.Bot.Builder.Testing enthalten.The DialogTestClient class is defined in the Microsoft.Bot.Builder.Testing namespace and included in the Microsoft.Bot.Builder.Testing NuGet package.

DialogTestClientDialogTestClient

Der erste Parameter von DialogTestClient ist der Zielkanal.The first parameter of DialogTestClient is the target channel. Auf diese Weise können Sie verschiedene Renderinglogik basierend auf dem Zielkanal für Ihren Bot (Teams, Slack usw.) testen.This allows you to test different rendering logic based on the target channel for your bot (Teams, Slack, etc.). Falls Sie in Bezug auf Ihren Zielkanal unsicher sind, können Sie die Kanal-IDs Emulator oder Test verwenden. Beachten Sie aber, dass sich einige Komponenten je nach aktuellem Kanal ggf. unterschiedlich verhalten. Mit ConfirmPrompt werden die Ja/Nein-Optionen für die Kanäle Test und Emulator beispielsweise unterschiedlich gerendert.If you are uncertain about your target channel, you can use the Emulator or Test channel ids but keep in mind that some components may behave differently depending on the current channel, for example, ConfirmPrompt renders the Yes/No options differently for the Test and Emulator channels. Sie können diesen Parameter auch nutzen, um die bedingte Renderinglogik in Ihrem Dialog anhand der Kanal-ID zu testen.You can also use this parameter to test conditional rendering logic in your dialog based on the channel ID.

Der zweite Parameter ist eine Instanz des zu testenden Dialogs. (Hinweis: „sut“ steht für „System Under Test“. Wir verwenden diese Abkürzung in den Codeausschnitten dieses Artikels.)The second parameter is an instance of the dialog being tested (Note: "sut" stands for "System Under Test", we use this acronym in the code snippets in this article).

Mit dem Konstruktor DialogTestClient werden zusätzliche Parameter angegeben, damit Sie das Clientverhalten weiter anpassen oder bei Bedarf Parameter an den zu testenden Dialog übergeben können.The DialogTestClient constructor provides additional parameters that allows you to further customize the client behavior or pass parameters to the dialog being tested if needed. Sie können Initialisierungsdaten für den Dialog übergeben, benutzerdefinierte Middleware hinzufügen oder Ihren eigenen TestAdapter und eine ConversationState-Instanz verwenden.You can pass initialization data for the dialog, add custom middleware or use your own TestAdapter and ConversationState instance.

Senden und Empfangen von NachrichtenSending and receiving messages

Mit der SendActivityAsync<IActivity>-Methode können Sie eine Textäußerung oder ein IActivity-Element an Ihren Dialog senden. Die Methode gibt die erste empfangene Nachricht zurück.The SendActivityAsync<IActivity> method allows you to send a text utterance or an IActivity to your dialog and returns the first message it receives. Der Parameter <T> wird verwendet, um eine stark typisierte Instanz der Antwort zurückzugeben, damit Sie sie bestätigen können, ohne sie umwandeln zu müssen.The <T> parameter is used to return a strong typed instance of the reply so you can assert it without having to cast it.

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

In einigen Szenarien sendet Ihr Bot in einer Antwort an eine Aktivität unter Umständen mehrere Nachrichten. Die Antworten werden von DialogTestClient dann in die Warteschlange eingereiht, und Sie können die GetNextReply<IActivity>-Methode verwenden, um die nächste Nachricht aus der Antwortwarteschlange abzurufen.In some scenarios your bot may send several messages in response to a single activity, in these cases DialogTestClient will queue the replies and you can use the GetNextReply<IActivity> method to pop the next message from the response queue.

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

GetNextReply<IActivity> gibt „null“ zurück, falls die Antwortwarteschlange keine weiteren Nachrichten enthält.GetNextReply<IActivity> will return null if there are no further messages in the response queue.

Bestätigen von AktivitätenAsserting activities

Im Code des CoreBot-Beispiels wird nur die Text-Eigenschaft der zurückgegebenen Aktivitäten bestätigt.The code in the CoreBot sample only asserts the Text property of the returned activities. Bei komplexeren Bots kann es ratsam sein, auch andere Eigenschaften zu bestätigen, z. B. Speak, InputHint, ChannelData usw.In more complex bots you may want to assert other properties like Speak, InputHint, ChannelData, etc.

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

Um dies zu erreichen, können Sie jede Eigenschaft wie oben gezeigt einzeln überprüfen. Sie können Ihre eigenen Hilfsprogramme zum Bestätigen von Aktivitäten schreiben, oder Sie können andere Frameworks verwenden, z. B. FluentAssertions, um benutzerdefinierte Assertionen zu schreiben und Ihren Testcode zu vereinfachen.You can do this by checking each property individually as shown above, you can write your own helper utilities for asserting activities or you can use other frameworks like FluentAssertions to write custom assertions and simplify your test code.

Übergeben von Parametern an Ihre DialogePassing parameters to your dialogs

Der Konstruktor DialogTestClient verfügt über ein initialDialogOptions-Element, das verwendet werden kann, um Parameter an Ihren Dialog zu übergeben.The DialogTestClient constructor has an initialDialogOptions that can be used to pass parameters to your dialog. Mit dem MainDialog-Element in diesem Beispiel wird beispielsweise ein BookingDetails-Objekt aus den LUIS-Ergebnissen mit den Entitäten initialisiert, die aus der Äußerung des Benutzers aufgelöst werden. Dieses Objekt wird dann im Aufruf übergeben, um BookingDialog aufzurufen.For example, the MainDialog in this sample, initializes a BookingDetails object from the LUIS results with the entities it resolves from the user's utterance and passes this object in the call to invoke BookingDialog.

Sie können dies in einem Test wie folgt implementieren:You can implement this in a test as follows:

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

Das BookingDialog-Element empfängt diesen Parameter und greift im Test genauso darauf zu, als ob es über MainDialog aufgerufen wird.BookingDialog receives this parameter and accesses it in the test the same way as it would have been when invoked from MainDialog.

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

Bestätigen der Ergebnisse von Dialog-TurnsAsserting dialog turn results

Einige Dialoge, z. B. BookingDialog oder DateResolverDialog, geben einen Wert an den aufrufenden Dialog zurück.Some dialogs like BookingDialog or DateResolverDialog return a value to the calling dialog. Das DialogTestClient-Objekt macht eine DialogTurnResult-Eigenschaft verfügbar, die zum Analysieren und Bestätigen der vom Dialog zurückgegebenen Ergebnisse verwendet werden kann.The DialogTestClient object exposes a DialogTurnResult property that can be used to analyze and assert the results returned by the dialog.

Beispiel:For example:

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

Die DialogTurnResult-Eigenschaft kann auch verwendet werden, um Zwischenergebnisse zu untersuchen und zu bestätigen, die von den Schritten eines Wasserfalls zurückgegeben werden.The DialogTurnResult property can also be used to inspect and assert intermediate results returned by the steps in a waterfall.

Analysieren der TestausgabeAnalyzing test output

In einigen Fällen ist es erforderlich, ein Transkript eines Komponententests zu lesen, um die Testausführung zu analysieren, ohne den Test debuggen zu müssen.Sometimes it is necessary to read a unit test transcript to analyze the test execution without having to debug the test.

Das Paket Microsoft.Bot.Builder.Testing enthält einen XUnitDialogTestLogger zum Protokollieren der Nachrichten, die vom Dialog empfangen und an die Konsole gesendet werden.The Microsoft.Bot.Builder.Testing package includes a XUnitDialogTestLogger that logs the messages sent and received by the dialog to the console.

Zur Nutzung dieser Middleware muss Ihr Test einen Konstruktor verfügbar machen, der ein vom XUnit Test Runner bereitgestelltes ITestOutputHelper-Objekt empfängt. Außerdem muss ein XUnitDialogTestLogger erstellt werden, der über den Parameter middlewares an DialogTestClient übergeben wird.To use this middleware, your test needs to expose a constructor that receives an ITestOutputHelper object that is provided by the XUnit test runner and create a XUnitDialogTestLogger that will be passed to DialogTestClient through the middlewares parameter.

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

        ...
    }
}

Hier ist ein Beispiel dafür angegeben, was vom XUnitDialogTestLogger im Ausgabefenster protokolliert wird, wenn dies konfiguriert ist:Here is an example of what the XUnitDialogTestLogger logs to the output window when it is configured:

Middlewareausgabe von XUnit

Weitere Informationen zum Senden von Testausgaben an die Konsole bei Verwendung von XUnit finden Sie in der XUnit-Dokumentation unter Capturing Output (Erfassen der Ausgabe).For additional information on sending test output to the console when using XUnit see Capturing Output in the XUnit documentation.

Diese Ausgabe wird auch bei Continuous Integration-Builds auf dem Buildserver protokolliert und dient Ihnen als Hilfe beim Analysieren von Buildfehlern.This output will be also logged on the build server during the continuous integration builds and helps you analyze build failures.

Datengesteuerte TestsData Driven Tests

In den meisten Fällen ändert sich die Dialoglogik nicht, und die unterschiedlichen Ausführungspfade einer Konversation basieren auf den Benutzeräußerungen.In most cases the dialog logic doesn't change and the different execution paths in a conversation are based on the user utterances. Anstatt nur einen Komponententest für jede Variante der Konversation zu schreiben, ist es einfacher, datengesteuerte Tests zu verwenden (auch als parametrisierte Tests bezeichnet).Rather than writing a single unit test for each variant in the conversation it is easier to use data driven tests (also known as parameterized test).

Mit dem Beispieltest im Übersichtsabschnitt dieses Dokuments wird beispielsweise veranschaulicht, wie Sie einen Ausführungsablauf testen. Aber was passiert, wenn der Benutzer beispielsweise die Bestätigung ablehnt oder ein anderes Datum verwendet?For example, the sample test in the overview section of this document shows how to test one execution flow, but what happens if the user says no to the confirmation? what if they use a different date? etc.

Mit datengesteuerten Tests können wir all diese unterschiedlichen Fälle testen, ohne die Tests umschreiben zu müssen.Data driven tests allow us to test all these permutations without having to rewrite the tests.

Im CoreBot-Beispiel verwenden wir Theory-Tests aus XUnit,um die Tests zu parametrisieren.In the CoreBot sample, we use Theory tests from XUnit to parameterize tests.

Theory-Tests mit InlineDataTheory tests using InlineData

Mit dem folgenden Test wird überprüft, ob ein Dialog abgebrochen wird, wenn der Benutzer „Cancel“ (Abbrechen) sagt.The following test checks that a dialog gets cancelled when the user says "cancel".

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

Zum Abbrechen eines Dialogs (auf Englisch) können Benutzer „quit“, „never mind“ und „stop it“ eingeben.To cancel a dialog, users can type "quit", "never mind", and "stop it". Sie müssen nicht für jedes mögliche Wort einen neuen Testfall schreiben, sondern lediglich eine Theory-Testmethode, die Parameter aus einer Liste mit InlineData-Werten akzeptiert. Hiermit werden die Parameter für die einzelnen Testfälle definiert:Rather then writing a new test case for every possible word, write a single Theory test method that accepts parameters via a list of InlineData values to define the parameters for each test case:

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

Der neue Test wird viermal mit den unterschiedlichen Parametern durchgeführt, und jeder Fall wird in Visual Studio-Test-Explorer unter ShouldBeAbleToCancel als untergeordnetes Element angezeigt.The new test will be executed 4 times with the different parameters and each case will show as a child item under the ShouldBeAbleToCancel test in Visual Studio Test Explorer. Falls Tests wie unten gezeigt nicht erfolgreich sind, können Sie mit der rechten Maustaste klicken und das Fehlerszenario debuggen, anstatt den gesamten Testsatz erneut auszuführen.If any of them fail as shown below, you can right click and debug the scenario that failed rather than re-running the entire set of tests.

Testergebnisse für Inlinedaten

Theory-Tests mit MemberData und komplexen TypenTheory tests using MemberData and complex types

InlineData ist für kleinere datengesteuerte Tests hilfreich, bei denen einfache Werttypparameter („string“, „int“ usw.) empfangen werden.InlineData is useful for small data driven tests that receive simple value type parameters (string, int, etc.).

Das BookingDialog-Element empfängt ein BookingDetails-Objekt und gibt ein neues BookingDetails-Objekt zurück.The BookingDialog receives a BookingDetails object and returns a new BookingDetails object. Eine nicht parametrisierte Version eines Tests für diesen Dialog sieht wie folgt aus:A non-parameterized version of a test for this dialog would look as follows:

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

Zum Parametrisieren dieses Tests haben wir eine BookingDialogTestCase-Klasse erstellt, die unsere Testfalldaten enthält.To parameterize this test, we created a BookingDialogTestCase class that contains our test case data. Sie enthält das ursprüngliche BookingDetails-Objekt, die erwarteten BookingDetails und ein Array mit Zeichenfolgen, die die vom Benutzer gesendeten Äußerungen und die erwarteten Antworten aus dem Dialog für jeden Turn umfassen.It contains the initial BookingDetails object, the expected BookingDetails and an array of strings containing the utterances sent from the user and the expected replies from the dialog for each turn.

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

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

    public BookingDetails ExpectedBookingDetails { get; set; }
}

Wir haben auch die Hilfsklasse BookingDialogTestsDataGenerator erstellt, die eine IEnumerable<object[]> BookingFlows()-Methode verfügbar macht. Hiermit wird eine Sammlung mit den Testfällen zurückgegeben, die vom Test verwendet werden.We also created a helper BookingDialogTestsDataGenerator class that exposes a IEnumerable<object[]> BookingFlows() method that returns a collection of the test cases to be used by the test.

Um in Visual Studio-Test-Explorer jeden Testfall als einzelnes Element anzeigen zu können, ist es für den XUnit Test Runner erforderlich, dass für komplexe Typen wie BookingDialogTestCase das IXunitSerializable-Element implementiert wird. Zur Vereinfachung wird über das Bot.Builder.Testing-Framework eine TestDataObject-Klasse bereitgestellt, mit der diese Schnittstelle implementiert wird und die genutzt werden kann, um die Testfalldaten ohne Implementierung von IXunitSerializable zu umschließen.In order to display each test case as a separate item in Visual Studio Test Explorer, the XUnit test runner requires that complex types like BookingDialogTestCase implement IXunitSerializable, to simplify this, the Bot.Builder.Testing framework provides a TestDataObject class that Implements this interface and can be used to wrap the test case data without having to implement IXunitSerializable.

Hier ist ein Fragment von IEnumerable<object[]> BookingFlows() angegeben, mit dem verdeutlicht wird, wie die beiden Klassen verwendet werden:Here is a fragment of IEnumerable<object[]> BookingFlows() that shows how the two classes are used:

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

Nachdem wir ein Objekt zum Speichern der Testdaten und eine Klasse erstellt haben, die eine Sammlung mit Testfällen verfügbar macht, verwenden wir das XUnit-Attribut MemberData anstelle von InlineData, um die Daten in den Test einzubinden. Der erste Parameter für MemberData ist der Name der statischen Funktion, mit der die Sammlung mit den Testfällen zurückgegeben wird, und der zweite Parameter ist der Typ der Klasse, mit dem diese Methode verfügbar gemacht wird.Once we create an object to store the test data and a class that exposes a collection of test cases, we use the XUnit MemberData attribute instead of InlineData to feed the data into the test, the first parameter for MemberData is the name of the static function that returns the collection of test cases and the second parameter is the type of the class that exposes this method.

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

Hier ist ein Beispiel für die Ergebnisse der DialogFlowUseCases-Tests in Visual Studio-Test-Explorer nach der Ausführung angegeben:Here is an example of the results for the DialogFlowUseCases tests in Visual Studio Test Explorer when the test is executed:

Beispielergebnisse für den Reservierungsdialog

Verwenden von PseudoelementenUsing Mocks

Für die Bereiche, die derzeit nicht getestet werden sollen, können Sie Pseudoelemente verwenden.You can use mock elements for the things that are not currently tested. Zu Referenzzwecken kann diese Ebene grundsätzlich als Einheiten- und Integrationstest betrachtet werden.For reference, this level can generally be thought of as unit and integration testing.

Wenn Sie so viele Elemente wie möglich modellieren, können Sie das getestete Element besser isolieren.Mocking as many elements as you can allows for better isolation of the piece you're testing. Zu den Kandidaten für Modellelemente gehören Speicher, Adapter, Middleware, Aktivitätspipeline, Kanäle und alle weitere Elemente, die nicht direkt zu Ihrem Bot gehören.Candidates for mock elements include storage, the adapter, middleware, activity pipeline, channels, and anything else that is not directly part of your bot. Dies kann auch das vorübergehende Entfernen bestimmter Aspekte umfassen, um jedes Element zu isolieren. Dies kann beispielsweise Middleware sein, die nicht zu den zu testenden Bereichen Ihres Bots gehört.This could also involve removing certain aspects temporarily, such as middleware not involved in the part of your bot that you are testing, to isolate each piece. Wenn Sie Ihre Middleware testen, sollten Sie jedoch möglicherweise stattdessen Ihren Bot modellieren.However, if you are testing your middleware, you may want to mock your bot instead.

Das Modellieren von Elementen kann eine Reihe von Formen annehmen, vom Ersetzen eines Elements durch ein anderes bekanntes Objekt bis hin zum Implementieren einer grundlegenden Hallo Welt-Funktionalität.Mocking elements can take a handful of forms, from replacing an element with a different known object to implementing a bare bones hello world functionality. Dies kann auch die Form des einfachen Entfernens des Elements haben, wenn es nicht erforderlich ist, oder einfach dazu zwingen, nichts zu tun.This could also take the form of simply removing the element, if it's not necessary, or simply force it to do nothing.

Mit Pseudoelementen können wir die Abhängigkeiten eines Dialogs konfigurieren und sicherstellen, dass diese sich während der Durchführung des Tests in einem bekannten Zustand befinden, ohne externe Ressourcen wie Datenbanken, LUIS-Modelle oder andere Objekte verwenden zu müssen.Mocks allow us to configure the dependencies of a dialog and ensure they are in a known state during the execution of the test without having to rely on external resources like databases, LUIS models or other objects.

Um für Ihren Dialog das Testen zu vereinfachen und dessen Abhängigkeiten von externen Objekten zu reduzieren, müssen Sie unter Umständen externe Abhängigkeiten in den Konstruktor des Dialogs einfügen.In order to make your dialog easier to test and reduce its dependencies on external objects, you may need to inject the external dependencies in the dialog constructor.

Beispiel: Anstelle der Instanziierung von BookingDialog in MainDialog:For example, instead of instantiating BookingDialog in MainDialog:

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

Übergeben wir eine Instanz von BookingDialog als Konstruktorparameter:We pass an instance of BookingDialog as a constructor parameter:

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

Wir können dann die BookingDialog-Instanz durch ein Pseudoobjekt ersetzen und Komponententests für MainDialog schreiben, ohne die eigentliche BookingDialog-Klasse aufzurufen.This allow us to replace the BookingDialog instance with a mock object and write unit tests for MainDialog without having to call the actual BookingDialog class.

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

Verwenden von Pseudoelementen für DialogeMocking Dialogs

Wie oben beschrieben wird von MainDialog das BookingDialog-Element aufgerufen, um das BookingDetails-Objekt zu erhalten.As described above, MainDialog invokes BookingDialog to obtain the BookingDetails object. Wir implementieren und konfigurieren wie folgt eine Pseudoinstanz von BookingDialog:We implement and configure a mock instance of BookingDialog as follows:

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

In diesem Beispiel haben wir Moq verwendet, um den Pseudodialog zu erstellen, und die Methoden Setup und Returns, um das Verhalten zu konfigurieren.In this example, we used Moq to create the mock dialog and the Setup and Returns methods to configure its behavior.

Verwenden von LUIS-PseudoergebnissenMocking LUIS results

In einfachen Szenarien können Sie LUIS-Pseudoergebnisse per Code wie folgt implementieren:In simple scenarios, you can implement mock LUIS results through code as follows:

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-Ergebnisse können aber komplex sein. Wenn dies der Fall ist, ist es einfacher, das gewünschte Ergebnis in einer JSON-Datei zu erfassen, es Ihrem Projekt als Ressource hinzuzufügen und es zu einem LUIS-Ergebnis zu deserialisieren.But LUIS results can be complex, and when they are it is simpler to capture the desired result in a json file, add it as a resource to your project and deserialize it into a LUIS result. Beispiel:Here is an example:

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

Zusätzliche InformationenAdditional information