如何對聊天機器人進行單元測試How to unit test bots

適用于: SDK v4APPLIES TO: SDK v4

在本主題中,我們將說明如何:In this topic we'll show you how to:

  • 為聊天機器人建立單元測試Create unit tests for bots
  • 使用判斷提示來檢查對話方塊回合針對預期值所傳回的活動Use assert to check for activities returned by a dialog turn against expected values
  • 使用判斷提示來檢查對話方塊所傳回的結果Use assert to check the results returned by a dialog
  • 建立不同類型的資料驅動測試Create different types of data driven tests
  • 為不同的對話相依性建立模擬物件 (亦即 LUIS 辨識器等)Create mock objects for the different dependencies of a dialog (i.e. LUIS recognizers, etc.)

PrerequisitesPrerequisites

本主題所用的 CoreBot Tests 範例會參考 Microsoft.Bot.Builder.Testing 套件、XUnit 和用來建立單元測試的 MoqThe CoreBot Tests sample used in this topic references the Microsoft.Bot.Builder.Testing package, XUnit, and Moq to create unit tests.

測試對話方塊Testing Dialogs

在 CoreBot 範例中,對話方塊會透過 DialogTestClient 類別來進行單元測試,其所提供的機制可供在聊天機器人外部隔離測試對話,而不需要將程式碼部署至 Web 服務。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.

使用這個類別,您就可以撰寫單元測試來逐一回合地驗證回應的對話。Using this class, you can write unit tests that validate dialogs responses on a turn-by-turn basis. 使用 DialogTestClient 類別的單元測試應該要能與使用 botbuilder 對話方塊程式庫所建立的其他對話方塊搭配運作。Unit tests using DialogTestClient class should work with other dialogs built using the botbuilder dialogs library.

下列範例示範衍生自 DialogTestClient 的測試: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);

DialogTestClient 類別會定義在 Microsoft.Bot.Builder.Testing 命名空間中,並包含在 Microsoft.Bot.Builder.Testing NuGet 套件中。The DialogTestClient class is defined in the Microsoft.Bot.Builder.Testing namespace and included in the Microsoft.Bot.Builder.Testing NuGet package.

DialogTestClientDialogTestClient

DialogTestClient 的第一個參數是目標通道。The first parameter of DialogTestClient is the target channel. 這可讓您根據 bot 的目標通道來測試不同的轉譯邏輯 (團隊、時差等 ) 。This allows you to test different rendering logic based on the target channel for your bot (Teams, Slack, etc.). 如果您不確定目標通道為何,則可以使用 EmulatorTest 通道識別碼,但請記住,某些元件可能會因為目前的通道而有不同的行為,例如,ConfirmPrompt 會針對 TestEmulator 通道呈現不同的 [是/否] 選項。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. 您也可以使用這個參數,來根據通道識別碼測試對話方塊中的條件式呈現邏輯。You can also use this parameter to test conditional rendering logic in your dialog based on the channel ID.

第二個參數是所測試對話方塊的執行個體 (注意: "sut" 代表 "System Under Test" (待測系統),我們會在本文的程式碼片段中使用此縮略字)。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).

DialogTestClient 建構函式會提供其他參數,以便讓您進一步自訂用戶端行為,或在需要時將參數傳遞給所測試的對話方塊。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. 您可以傳遞對話方塊的初始化資料、新增自訂中介軟體,或使用您自己的 TestAdapter 和 ConversationState 執行個體。You can pass initialization data for the dialog, add custom middleware or use your own TestAdapter and ConversationState instance.

傳送和接收訊息Sending and receiving messages

SendActivityAsync<IActivity> 方法可讓您將文字表達或 IActivity 傳送給對話方塊,並傳回其所收到的第一則訊息。The SendActivityAsync<IActivity> method allows you to send a text utterance or an IActivity to your dialog and returns the first message it receives. <T> 參數可用來傳回強型別的回覆執行個體,因此您不必轉換就可以對其做出判斷提示。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);

在某些情況下,聊天機器人可能會傳送數則訊息來回應單一活動,在這些情況下,DialogTestClient 會將回覆排入佇列,而您可以使用 GetNextReply<IActivity> 方法從回應佇列中快顯下一則訊息。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> 會傳回 Null。GetNextReply<IActivity> will return null if there are no further messages in the response queue.

活動的判斷提示Asserting activities

CoreBot 範例中的程式碼只會對所傳回活動的 Text 屬性進行判斷提示。The code in the CoreBot sample only asserts the Text property of the returned activities. 在更複雜的聊天機器人中,您可以針對 SpeakInputHintChannelData 等其他屬性進行判斷提示。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);

您可以如上所示逐一檢查每個屬性來進行此操作、您可以撰寫自己的協助程式公用程式來對活動進行判斷提示,您也可以使用 FluentAssertions 等其他架構來撰寫自訂判斷提示並簡化測試程式碼。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.

將參數傳遞給對話方塊Passing parameters to your dialogs

DialogTestClient 建構函式具有的 initialDialogOptions 可用來將參數傳遞給對話方塊。The DialogTestClient constructor has an initialDialogOptions that can be used to pass parameters to your dialog. 例如,此範例中的 MainDialog 會使用從使用者的表達解析得到的實體將 LUIS 結果中的 BookingDetails 物件初始化,然後將此物件傳遞至呼叫中以叫用 BookingDialogFor 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.

您可以在測試中執行此項目,如下所示: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);

BookingDialog 會接收此參數並在測試中加以存取,方式就和從 MainDialog 叫用時一樣。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;
    ...
}

對話回合結果的判斷提示Asserting dialog turn results

某些對話方塊 (如 BookingDialogDateResolverDialog) 會對呼叫端對話方塊傳回值。Some dialogs like BookingDialog or DateResolverDialog return a value to the calling dialog. DialogTestClient 物件會公開 DialogTurnResult 屬性,以供用來對對話方塊所傳回的結果進行分析和判斷提示。The DialogTestClient object exposes a DialogTurnResult property that can be used to analyze and assert the results returned by the dialog.

例如: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);

DialogTurnResult 屬性也可用來對瀑布中的步驟,所傳回的中繼結果進行檢查和判斷提示。The DialogTurnResult property can also be used to inspect and assert intermediate results returned by the steps in a waterfall.

分析測試輸出Analyzing test output

系統有時候必須讀取單元測試文字記錄以便分析測試的執行情形,而不需要對測試進行偵錯。Sometimes it is necessary to read a unit test transcript to analyze the test execution without having to debug the test.

Microsoft.Bot.Builder.Testing 套件包含 XUnitDialogTestLogger,其會將對話所傳送和接收的訊息記錄到主控台。The Microsoft.Bot.Builder.Testing package includes a XUnitDialogTestLogger that logs the messages sent and received by the dialog to the console.

若要使用這個中介軟體,測試作業需要公開一個建構函式 (以接收 XUnit 測試執行器所提供的 ITestOutputHelper 物件),並建立會透過 middlewares 參數傳遞至 DialogTestClientXUnitDialogTestLoggerTo 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);

        ...
    }
}

以下範例指出 XUnitDialogTestLogger 在設定時會記錄至輸出視窗的內容:Here is an example of what the XUnitDialogTestLogger logs to the output window when it is configured:

XUnit 的中介軟體輸出

如需使用 XUnit 時要如何將測試輸出傳送至主控台的其他資訊,請參閱 XUnit 文件中的擷取輸出For additional information on sending test output to the console when using XUnit see Capturing Output in the XUnit documentation.

在建置持續整合期間,此輸出也會記錄在組建伺服器上,並協助您分析失敗的建置。This output will be also logged on the build server during the continuous integration builds and helps you analyze build failures.

資料驅動測試Data Driven Tests

在大部分情況下,對話方塊邏輯都不會變更,而且對話方塊中的不同執行路徑都是以使用者表達作為基礎的。In most cases the dialog logic doesn't change and the different execution paths in a conversation are based on the user utterances. 您不必針對對話中的每一種變化撰寫單一的單元測試,更輕鬆的方式是使用資料驅動測試 (也稱為參數化測試)。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).

例如,本文件 [概觀] 區段中的測試範例會示範如何測試一個執行流程,但如果使用者拒絕確認會怎樣?如果使用者使用不同的日期會怎樣等等。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.

資料驅動測試可讓我們測試上述所有情形的排列組合,而不必重新撰寫測試。Data driven tests allow us to test all these permutations without having to rewrite the tests.

在 CoreBot 範例中,我們從 XUnit 使用 Theory 測試來將測試參數化。In the CoreBot sample, we use Theory tests from XUnit to parameterize tests.

使用 InlineData 來測試理論Theory tests using InlineData

下列測試會確認使用者回答「取消」時,對話是否會取消。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);
}

若要取消對話,使用者可以輸入「結束」、「沒關係」和「停下來」。To cancel a dialog, users can type "quit", "never mind", and "stop it". 您不必針對每個可能的單字撰寫新的測試案例,而只要撰寫單一的 Theory 測試方法,透過 InlineData 值的清單接受參數,以定義每個測試案例的參數: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);
}

新測試會使用不同參數執行 4 次,而且每個案例都會在 Visual Studio 測試總管中顯示為 ShouldBeAbleToCancel 測試底下的子項目。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. 如果其中任何一次失敗了 (如下所示),您可以對失敗的案例按一下滑鼠右鍵並進行偵錯,而不必重新執行整組測試。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.

內嵌資料的測試結果

使用 MemberData 和複雜類型的理論測試Theory tests using MemberData and complex types

InlineData 適用於會接收簡單實值型別參數 (字串、int 等) 的小型資料驅動測試。InlineData is useful for small data driven tests that receive simple value type parameters (string, int, etc.).

BookingDialog 會接收 BookingDetails 物件,並傳回新的 BookingDetails 物件。The BookingDialog receives a BookingDetails object and returns a new BookingDetails object. 此對話方塊測試的非參數化版本會如下所示: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);
}

為了將此測試參數化,我們建立了一個包含測試案例資料的 BookingDialogTestCase 類別。To parameterize this test, we created a BookingDialogTestCase class that contains our test case data. 其包含初始的 BookingDetails 物件、預期的 BookingDetails 和字串陣列,此字串陣列包含使用者所傳來的表達,以及每一回合對話的預期回覆。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; }
}

我們也建立了一個協助程式 BookingDialogTestsDataGenerator 類別,其會公開 IEnumerable<object[]> BookingFlows() 方法,以傳回要供測試使用的測試案例集合。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.

為了在 Visual Studio 測試總管中分開顯示每個測試案例項目,XUnit 測試執行器會要求 BookingDialogTestCase 等複雜類型實作 IXunitSerializable,而為了簡化此程序,Bot.Builder.Testing 架構提供了 TestDataObject 類別,以實作此介面並可供用來包裝測試案例資料,而不必實作 IXunitSerializableIn 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.

以下是 IEnumerable<object[]> BookingFlows() 的片段,其可說明這兩個類別的使用方式: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) };
    }
}

在建立了用來儲存測試資料的物件以及會公開測試案例集合的類別之後,我們會使用 XUnit 的 MemberData 屬性 (而非 InlineData) 將資料饋送至測試中,MemberData 的第一個參數是靜態函式的名稱,其會傳回測試案例的集合,第二個參數則是類別的型別,其會公開這個方法。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);
}

以下是執行測試時,Visual Studio 測試總管中 DialogFlowUseCases 測試結果的範例:Here is an example of the results for the DialogFlowUseCases tests in Visual Studio Test Explorer when the test is executed:

預訂對話的範例結果

使用模擬Using Mocks

您可以將模擬元素用於目前未進行測試的項目。You can use mock elements for the things that are not currently tested. 做為參考,此等級通常可視為單元和整合測試。For reference, this level can generally be thought of as unit and integration testing.

您可以模擬多個專案,讓您能夠更有效地隔離正在測試的部分。Mocking as many elements as you can allows for better isolation of the piece you're testing. 模擬元素的對象包括儲存體、配接器、中介軟體、活動管線、通道,以及任何其他非直屬於 Bot 的組件。Candidates for mock elements include storage, the adapter, middleware, activity pipeline, channels, and anything else that is not directly part of your bot. 這也可能涉及暫時移除某些部分 (例如與您要測試之 Bot 無關的中介軟體),以便隔離每個部分。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. 不過,如果您要測試中介軟體,建議可以改為模擬 Bot。However, if you are testing your middleware, you may want to mock your bot instead.

模擬元素可以採取數種形式,從以不同的已知物件取代元素,到實作最基本的 Hello World 功能。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. 這也可以只是移除專案(如果不需要的話),或只強制執行任何動作。This could also take the form of simply removing the element, if it's not necessary, or simply force it to do nothing.

模擬可讓我們設定對話方塊的相依性,並確保相依性在測試執行期間處於已知狀態,而不必依賴資料庫、LUIS 模型或其他物件等外部資源。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.

為了讓對話方塊更容易進行測試,並減少其對外部物件的相依性,您可能需要將外部相依性插入到對話方塊建構函式中。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.

例如,不要在 MainDialog 中具現化 BookingDialogFor example, instead of instantiating BookingDialog in MainDialog:

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

我們會以建構函式參數的形式傳遞 BookingDialog 的執行個體:We pass an instance of BookingDialog as a constructor parameter:

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

這可讓我們將 BookingDialog 執行個體取代為模擬物件並撰寫 MainDialog 的單元測試,而不必呼叫實際的 BookingDialog 類別。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);

模擬對話方塊Mocking Dialogs

如上所述,MainDialog 會叫用 BookingDialog 來取得 BookingDetails 物件。As described above, MainDialog invokes BookingDialog to obtain the BookingDetails object. 我們會實作並設定 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);

在此範例中,我們使用了 Moq 來建立模擬對話方塊,並使用 SetupReturns 方法來設定其行為。In this example, we used Moq to create the mock dialog and the Setup and Returns methods to configure its behavior.

模擬 LUIS 結果Mocking LUIS results

在簡單案例中,您可以透過程式碼來實作模擬 LUIS 結果,如下所示: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 結果可能會很複雜,如果確實如此,您可以簡化為以 json 檔案擷取想要的結果、將其作為資源新增至專案中,然後將其還原序列化為 LUIS 結果。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. 範例如下: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);
    });

其他資訊Additional information