Cómo hacer pruebas unitarias de botsHow to unit test bots

SE APLICA A: SíSDK v4 No SDK v3APPLIES TO: yesSDK v4 no SDK v3

En este tema se mostrará cómo:In this topic we'll show you how to:

  • Crear pruebas unitarias para bots.Create unit tests for bots
  • Usar aserciones para comprobar si las actividades devueltas por un diálogo se encuentran dentro de los valores esperados.Use assert to check for activities returned by a dialog turn against expected values
  • Usar aserciones para comprobar los resultados devueltos por un diálogo.Use assert to check the results returned by a dialog
  • Crear diferentes tipos de pruebas controladas por datos.Create different types of data driven tests
  • Crear objetos ficticios para las diferentes dependencias de un diálogo (es decir, reconocedores de LUIS, etc.).Create mock objects for the different dependencies of a dialog (i.e. LUIS recognizers, etc.)

PrerrequisitosPrerequisites

El ejemplo CoreBot Tests que se usa en este tema utiliza el paquete Microsoft.Bot.Builder.Testing, XUnit y Moq para crear pruebas unitarias.The CoreBot Tests sample used in this topic references the Microsoft.Bot.Builder.Testing package, XUnit, and Moq to create unit tests.

Pruebas de diálogosTesting Dialogs

En el ejemplo CoreBot, las pruebas unitarias de los diálogos se realizan con la clase DialogTestClient, que proporciona un mecanismo para probarlos de forma aislada fuera de un bot y sin tener que implementar el código en un servicio 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.

Con esta clase, puede escribir pruebas unitarias que validen las respuestas de los diálogos por turnos.Using this class, you can write unit tests that validate dialogs responses on a turn-by-turn basis. Las pruebas unitarias que usan la clase DialogTestClient deben funcionar con otros diálogos creados con la biblioteca de diálogos de botbuilder.Unit tests using DialogTestClient class should work with other dialogs built using the botbuilder dialogs library.

En el ejemplo siguiente se muestran las pruebas derivadas de 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);

La clase DialogTestClient se define en el espacio de nombres Microsoft.Bot.Builder.Testing y se incluye en el paquete de NuGet Microsoft.Bot.Builder.Testing.The DialogTestClient class is defined in the Microsoft.Bot.Builder.Testing namespace and included in the Microsoft.Bot.Builder.Testing NuGet package.

DialogTestClientDialogTestClient

El primer parámetro de DialogTestClient es el canal de destino.The first parameter of DialogTestClient is the target channel. Esto le permite probar una lógica de representación diferente en función del canal de destino del bot (Teams, Slack, Cortana, etc.).This allows you to test different rendering logic based on the target channel for your bot (Teams, Slack, Cortana, etc.). Si no está seguro de cuál es su canal de destino, puede usar los identificadores de canal Emulator o Test, pero tenga en cuenta que algunos componentes se comportarán de forma diferente en función del canal actual; por ejemplo, ConfirmPrompt representa las opciones sí/no de forma diferente para los canales Test y 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. También puede usar este parámetro para probar la lógica de representación condicional en el diálogo en función del identificador de canal.You can also use this parameter to test conditional rendering logic in your dialog based on the channel ID.

El segundo parámetro es una instancia del diálogo que se está probando. (Nota: "sut" significa "sistema en pruebas"; usamos este acrónimo en los fragmentos de código de este artículo).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).

El constructor DialogTestClient proporciona parámetros adicionales que le permiten personalizar aún más el comportamiento del cliente o pasar parámetros al diálogo que se está probando, si es necesario.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. Puede pasar los datos de inicialización del diálogo, agregar middleware personalizado o usar su propio TestAdapter y su propia instancia de ConversationState.You can pass initialization data for the dialog, add custom middleware or use your own TestAdapter and ConversationState instance.

Envío y recepción de mensajesSending and receiving messages

El método SendActivityAsync<IActivity> permite enviar una expresión de texto o un IActivity al diálogo y devuelve el primer mensaje que recibe.The SendActivityAsync<IActivity> method allows you to send a text utterance or an IActivity to your dialog and returns the first message it receives. El parámetro <T> se usa para devolver una instancia fuertemente tipada de la respuesta para que pueda validarla sin tener que convertirla.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);

En algunos escenarios, el bot puede enviar varios mensajes en respuesta a una sola actividad; en estos casos, DialogTestClient pondrá en cola las respuestas y puede usar el método GetNextReply<IActivity> para extraer el siguiente mensaje de la cola de respuesta.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> devolverá null si no hay más mensajes en la cola de respuesta.GetNextReply<IActivity> will return null if there are no further messages in the response queue.

Aserción de actividadesAsserting activities

El código del ejemplo CoreBot solo valida la propiedad Text de las actividades devueltas.The code in the CoreBot sample only asserts the Text property of the returned activities. En bots más complejos, es posible que desee validar otras propiedades, como Speak, InputHint o ChannelData, por ejemplo.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);

Para ello, puede comprobar cada propiedad individualmente como se muestra arriba, puede escribir sus propias utilidades auxiliares para la aserción de actividades o puede usar otras plataformas, como FluentAssertions, para escribir aserciones personalizadas y simplificar el código de prueba.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.

Paso de parámetros a los diálogosPassing parameters to your dialogs

El constructor DialogTestClient tiene initialDialogOptions que se puede usar para pasar parámetros al diálogo.The DialogTestClient constructor has an initialDialogOptions that can be used to pass parameters to your dialog. Por ejemplo, en este ejemplo, MainDialog inicializa un objeto BookingDetails a partir de los resultados de LUIS con las entidades que resuelve a partir de la expresión del usuario, y pasa este objeto en la llamada a 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.

Puede implementarlo en una prueba de la siguiente manera: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 recibe este parámetro y accede a él en la prueba de la misma manera que si se hubiera invocado desde 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;
    ...
}

Aserción de los resultados del turno de diálogoAsserting dialog turn results

Algunos diálogos, como BookingDialog o DateResolverDialog, devuelven un valor al diálogo que realiza la llamada.Some dialogs like BookingDialog or DateResolverDialog return a value to the calling dialog. El objeto DialogTestClient expone una propiedad DialogTurnResult que se puede utilizar para analizar y validar los resultados devueltos por el diálogo.The DialogTestClient object exposes a DialogTurnResult property that can be used to analyze and assert the results returned by the dialog.

Por ejemplo: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);

La propiedad DialogTurnResult también se puede utilizar para inspeccionar y validar los resultados intermedios devueltos por los pasos de una cascada.The DialogTurnResult property can also be used to inspect and assert intermediate results returned by the steps in a waterfall.

Análisis de la salida de la pruebaAnalyzing test output

A veces es necesario leer una transcripción de la prueba unitaria para analizar la ejecución sin tener que depurar la prueba.Sometimes it is necessary to read a unit test transcript to analyze the test execution without having to debug the test.

El paquete Microsoft.Bot.Builder.Testing incluye XUnitDialogTestLogger, que registra los mensajes enviados y recibidos por el diálogo en la consola.The Microsoft.Bot.Builder.Testing package includes a XUnitDialogTestLogger that logs the messages sent and received by the dialog to the console.

Para usar este middleware, la prueba debe exponer un constructor que recibe un objeto ITestOutputHelper proporcionado por el ejecutor de pruebas XUnit, y crear un XUnitDialogTestLogger que se pasará a DialogTestClient en el parámetro middlewares.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);

        ...
    }
}

Este es un ejemplo de lo que XUnitDialogTestLogger registra en la ventana de salida cuando se configura:Here is an example of what the XUnitDialogTestLogger logs to the output window when it is configured:

XUnitMiddlewareOutput

Para más información sobre cómo enviar la salida de la prueba a la consola cuando se usa XUnit, consulte Captura de la salida en la documentación de XUnit.For additional information on sending test output to the console when using XUnit see Capturing Output in the XUnit documentation.

Esta salida también se registrará en el servidor de compilación durante las compilaciones de integración continua, y le ayudará a analizar los errores de compilación.This output will be also logged on the build server during the continuous integration builds and helps you analyze build failures.

Pruebas controladas por datosData Driven Tests

En la mayoría de los casos, la lógica de los diálogos no cambia y las distintas rutas de ejecución de una conversación se basan en expresiones del usuario.In most cases the dialog logic doesn't change and the different execution paths in a conversation are based on the user utterances. En lugar de escribir una prueba unitaria única para cada variante de la conversación, es más fácil usar pruebas controladas por datos (también conocidas como pruebas parametrizadas).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).

En la prueba de ejemplo de la sección Información general de este documento se muestra cómo probar un flujo de ejecución, pero ¿qué sucede si el usuario responde no a la confirmación? o ¿qué ocurre si usan una fecha diferente?, por ejemplo.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.

Las pruebas controladas por datos nos permiten probar todas estas permutaciones sin tener que volver a escribir las pruebas.Data driven tests allow us to test all these permutations without having to rewrite the tests.

En el ejemplo CoreBot, usamos pruebas Theory de XUnit para parametrizar las pruebas.In the CoreBot sample, we use Theory tests from XUnit to parameterize tests.

Pruebas teóricas mediante InlineDataTheory tests using InlineData

La prueba siguiente comprueba que un diálogo se cancela cuando el usuario dice "Cancelar".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);
}

Para cancelar un diálogo, los usuarios pueden escribir "salir", "déjalo" y "para".To cancel a dialog, users can type "quit", "never mind", and "stop it". En lugar de escribir un nuevo caso de prueba para cada palabra posible, escriba un único método de prueba Theory que acepte parámetros de una lista de valores InlineData para definir los parámetros para cada caso de prueba: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);
}

La nueva prueba se ejecutará cuatro veces con los distintos parámetros y cada caso se mostrará como un elemento secundario en la prueba ShouldBeAbleToCancel, en el explorador de pruebas de Visual Studio.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. Si se produce un error en cualquiera de ellos, tal y como se muestra a continuación, puede hacer clic con el botón derecho y depurar el escenario en el que se produjo un error en lugar de volver a ejecutar todo el conjunto de pruebas.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.

InlineDataTestResults

Pruebas teóricas mediante MemberData y tipos complejosTheory tests using MemberData and complex types

InlineData es útil para realizar pruebas pequeñas controladas por datos que reciben parámetros de tipo de valor simple (String, int, etc.).InlineData is useful for small data driven tests that receive simple value type parameters (string, int, etc.).

BookingDialog recibe un objeto BookingDetails y devuelve un nuevo objeto BookingDetails.The BookingDialog receives a BookingDetails object and returns a new BookingDetails object. La versión sin parámetros de una prueba para este diálogo tendría el siguiente aspecto: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);
}

Para parametrizar esta prueba, creamos una clase BookingDialogTestCase que contiene los datos de los casos de prueba.To parameterize this test, we created a BookingDialogTestCase class that contains our test case data. Contiene el objeto BookingDetails inicial, el BookingDetails esperado y una matriz de cadenas que contienen las expresiones enviadas por el usuario y las respuestas que se espera del diálogo en cada turno.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; }
}

También hemos creado una clase auxiliar BookingDialogTestsDataGenerator que expone un método IEnumerable<object[]> BookingFlows() que devuelve una colección de los casos que se van a usar en la prueba.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.

Para mostrar cada caso de prueba como un elemento independiente en el explorador de pruebas de Visual Studio, el ejecutor de pruebas XUnit requiere que los tipos complejos como BookingDialogTestCase implementen IXunitSerializable. Para simplificar esto, la plataforma Bot.Builder.Testing proporciona una clase TestDataObject que implementa esta interfaz y que se puede usar para encapsular los datos del caso sin tener que implementar IXunitSerializable.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.

Este es un fragmento de IEnumerable<object[]> BookingFlows() que muestra cómo se usan las dos clases: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) };
    }
}

Una vez que se crea un objeto para almacenar los datos de prueba y una clase que expone una colección de casos de prueba, usamos el atributo MemberData de XUnit en lugar de InlineData para insertar los datos en la prueba. El primer parámetro de MemberData es el nombre de la función estática que devuelve la colección de casos de prueba, y el segundo parámetro es el tipo de la clase que expone este método.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);
}

Este es un ejemplo de los resultados de las pruebas DialogFlowUseCases en el explorador de pruebas de Visual Studio cuando se ejecuta la prueba:Here is an example of the results for the DialogFlowUseCases tests in Visual Studio Test Explorer when the test is executed:

BookingDialogTests

Uso de elementos ficticiosUsing Mocks

Puede usar elementos ficticios para los aspectos que no está probando actualmente.You can use mock elements for the things that are not currently tested. Como referencia, este nivel puede considerarse generalmente como unidad y prueba de integración.For reference, this level can generally be thought of as unit and integration testing.

La simulación de tantos elementos como pueda permite un mejor aislamiento de la pieza que está probando.Mocking as many elements as you can allows for better isolation of the piece you’re testing. Los candidatos para los elementos ficticios incluyen el almacenamiento, el adaptador, el software intermedio, la canalización de actividades, los canales y cualquier otra cosa que no forme parte del bot directamente.Candidates for mock elements include storage, the adapter, middleware, activity pipeline, channels, and anything else that is not directly part of your bot. También se podrían quitar ciertos aspectos temporalmente, como el software intermedio no implicado en la parte del bot que está probando, para aislar cada fragmento.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. Sin embargo, si va a probar su software intermedio, es posible que quiera simular el bot en su lugar.However, if you are testing your middleware, you may want to mock your bot instead.

La simulación de elementos puede asumir formas diferentes, desde el reemplazo de un elemento por otro objeto conocido a la implementación de una funcionalidad básica de Hola mundo.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. También puede consistir en la eliminación del elemento simplemente, en caso de que no sea necesario, así como en forzarlo a no hacer nada.This could also take the form of simply removing the element, if it’s not necessary, or simply force it to do nothing.

Los elementos ficticios nos permiten configurar las dependencias de un diálogo y asegurarnos de que su estado es conocido durante la ejecución de la prueba sin tener que depender de recursos externos como bases de datos, modelos LUIS u otros objetos.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.

Para facilitar la prueba del diálogo y reducir las dependencias de objetos externos, es posible que tenga que insertar las dependencias externas en el constructor de diálogos.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.

Por ejemplo, en lugar de crear instancias de BookingDialog en MainDialog:For example, instead of instantiating BookingDialog in MainDialog:

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

Pasamos una instancia de BookingDialog como un parámetro de constructor:We pass an instance of BookingDialog as a constructor parameter:

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

Esto nos permite reemplazar la instancia BookingDialog con un objeto ficticio y escribir pruebas unitarias para MainDialog sin tener que llamar a la clase BookingDialog real.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);

Diálogos ficticiosMocking Dialogs

Como se describió anteriormente, MainDialog invoca a BookingDialog para obtener el objeto BookingDetails.As described above, MainDialog invokes BookingDialog to obtain the BookingDetails object. Implementamos y configuramos una instancia ficticia de BookingDialog de la siguiente manera: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);

En este ejemplo, usamos Moq para crear el diálogo ficticio, y los métodos Setup y Returns para configurar su comportamiento.In this example, we used Moq to create the mock dialog and the Setup and Returns methods to configure its behavior.

Resultados de LUIS ficticiosMocking LUIS results

En escenarios sencillos, puede implementar resultados de LUIS ficticios mediante código de la siguiente manera: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);
    });

Pero los resultados de LUIS pueden ser complejos y, cuando lo son, es más fácil capturar el resultado en un archivo JSON, agregarlo como un recurso al proyecto y deserializarlo en un resultado de 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. Este es un ejemplo: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);
    });

Información adicionalAdditional information