봇을 단위 테스트하는 방법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.)

사전 요구 사항Prerequisites

이 토픽에 사용되는 CoreBot 테스트 샘플은 Microsoft.Bot.Builder.Testing 패키지 XUnitMoq를 참조하여 단위 테스트를 만듭니다.The 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 클래스를 통해 수행되며, 이 클래스는 웹 서비스에 코드를 배포할 필요 없이 봇 외부에 격리된 상태로 테스트할 수 있는 메커니즘을 제공합니다.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. 이를 통해 봇의 대상 채널(Teams, Slack 등)에 따라 다양한 렌더링 논리를 테스트할 수 있습니다.This allows you to test different rendering logic based on the target channel for your bot (Teams, Slack, etc.). 대상 채널이 확실하지 않은 경우 Emulator 또는 Test 채널 ID를 사용해도 되지만, 현재 채널에 따라 일부 구성 요소가 다르게 동작할 수 있습니다. 예를 들어 ConfirmPromptTest 채널과 Emulator 채널의 예/아니요 옵션을 다르게 렌더링합니다.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. 이 매개 변수를 사용하여 채널 ID를 기반으로 대화 상자의 조건부 렌더링 논리를 테스트할 수도 있습니다.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. 좀 더 복잡한 봇에서는 Speak, InputHint, ChannelData 등의 다른 속성을 어설션할 수도 있습니다.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 개체를 초기화하고 이 개체를 호출에서 전달하여 BookingDialog를 호출합니다.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.

다음과 같이 테스트에서 이것을 구현할 수 있습니다.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

BookingDialog 또는 DateResolverDialog 같은 일부 대화 상자는 호출하는 대화에 값을 반환합니다.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 매개 변수를 통해 DialogTestClient에 전달할 XUnitDialogTestLogger를 만들어야 합니다.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);

        ...
    }
}

다음은 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". 가능한 단어마다 새로운 테스트 사례를 작성하는 대신, 다음과 같이 InlineData 값 목록을 통해 매개 변수를 수락하여 각 테스트 사례의 매개 변수를 정의하는 단일 Theory 테스트 메서드를 작성하세요.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는 단순 값 형식 매개 변수(string, int 등)를 수신하는 작은 데이터 기반 테스트에 유용합니다.InlineData is useful for small data driven tests that receive simple value type parameters (string, int, etc.).

BookingDialogBookingDetails 개체를 수신하고 새 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; }
}

앞에서 테스트에 사용할 테스트 사례 컬렉션을 반환하는 IEnumerable<object[]> BookingFlows() 메서드를 노출하는 도우미 BookingDialogTestsDataGenerator 클래스도 만들었습니다.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 프레임워크는 IXunitSerializable을 구현할 필요 없이 이 인터페이스를 구현하여 테스트 사례 데이터를 래핑하는 데 사용할 수 있는 TestDataObject 클래스를 제공합니다.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.

다음은 두 클래스의 사용 방법을 보여주는 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) };
    }
}

테스트 데이터를 저장할 개체와 테스트 사례 컬렉션을 노출하는 클래스를 만든 후에는 InlineData 대신 XUnit MemberData 특성을 사용하여 테스트에 데이터를 제공합니다. 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. 모의 요소의 후보에는 스토리지, 어댑터, 미들웨어, 활동 파이프라인, 채널 및 직접적으로 봇의 파트가 아닌 모든 것이 포함됩니다.Candidates for mock elements include storage, the adapter, middleware, activity pipeline, channels, and anything else that is not directly part of your 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. 그러나 미들웨어를 테스트 중이라면 모의 봇을 만들 수 있습니다.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에서 BookingDialog를 인스턴스화하는 대신 다음을 적용합니다.For 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 클래스를 호출할 필요 없이 BookingDialog 인스턴스를 모의 개체로 바꾸고 MainDialog의 단위 테스트를 작성할 수 있습니다.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

위에서 설명했듯이, MainDialogBookingDialog를 호출하여 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