Postup testování jednotek robotů

PLATÍ PRO: SDK v4

V tomto tématu vám ukážeme, jak:

  • Vytváření testů jednotek pro roboty
  • Pomocí výrazu assert zkontrolujte aktivity vrácené dialogovým oknem proti očekávaným hodnotám.
  • Pomocí příkazu assert zkontrolujte výsledky vrácené dialogem.
  • Vytvořte různé typy testů řízených daty.
  • Vytvořte napodobené objekty pro různé závislosti dialogového okna, jako jsou rozpoznávání jazyka atd.

Předpoklady

Ukázka testů CoreBotu použitá v tomto tématu odkazuje na balíček Microsoft.Bot.Builder.Testing , XUnit a Moq k vytvoření testů jednotek.

Ukázka základního robota používá službu Language Understanding (LUIS) k identifikaci záměrů uživatelů; ale identifikace záměru uživatele není fokusem tohoto článku. Informace o identifikaci záměrů uživatelů najdete v tématu Principy přirozeného jazyka a Přidání porozumění přirozenému jazyku do robota.

Poznámka:

Služba Language Understanding (LUIS) bude vyřazena 1. října 2025. Od 1. dubna 2023 nebudete moct vytvářet nové prostředky LUIS. Novější verze language understanding je teď dostupná jako součást jazyka Azure AI.

Konverzační jazyk understanding (CLU), funkce jazyka Azure AI, je aktualizovaná verze služby LUIS. Další informace o podpoře porozumění jazyku v sadě SDK služby Bot Framework najdete v tématu Principy přirozeného jazyka.

Testovací dialogy

V ukázce CoreBot se dialogy testují prostřednictvím DialogTestClient třídy, která poskytuje mechanismus pro jejich testování v izolaci mimo robota a bez nutnosti nasazovat kód do webové služby.

Pomocí této třídy můžete psát testy jednotek, které ověřují odpovědi na dialogy na základě zapnutí. Testy jednotek používající DialogTestClient třídu by měly fungovat s dalšími dialogy vytvořenými pomocí knihovny dialogových oken botbuilderu.

Následující příklad ukazuje testy odvozené z 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);

Třída DialogTestClient je definována Microsoft.Bot.Builder.Testing v oboru názvů a je součástí balíčku NuGet Microsoft.Bot.Builder.Testing .

DialogTestClient

Prvním parametrem DialogTestClient je cílový kanál. To vám umožní otestovat různé logiky vykreslování na základě cílového kanálu robota (Teams, Slack atd.). Pokud si nejste jistí, že váš cílový kanál, můžete použít Emulator ID kanálu, Test ale mějte na paměti, že některé komponenty se můžou chovat jinak v závislosti na aktuálním kanálu, ConfirmPrompt například vykreslí možnosti Ano/Ne pro kanály TestEmulator jinak. Tento parametr můžete použít také k otestování logiky podmíněného vykreslování v dialogovém okně na základě ID kanálu.

Druhým parametrem je instance testovaného dialogového okna. V ukázkovém kódu v tomto článku sut představuje systém, který se testuje.

Konstruktor DialogTestClient poskytuje další parametry, které umožňují dále přizpůsobit chování klienta nebo předat parametry do dialogového okna testovaného v případě potřeby. Můžete předat data inicializace dialogového okna, přidat vlastní middleware nebo použít vlastní TestAdapter a ConversationState instanci.

Odesílání a příjem zpráv

Metoda SendActivityAsync<IActivity> umožňuje odeslat textovou promluvu nebo do dialogového IActivity okna a vrátí první zprávu, kterou obdrží. Parametr <T> se používá k vrácení silné instance typu odpovědi, abyste ji mohli uplatnit, aniž byste ho museli přetypovat.

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

V některých scénářích může robot odeslat několik zpráv v reakci na jednu aktivitu, v těchto případech DialogTestClient zařadí odpovědi do fronty a pomocí této metody můžete GetNextReply<IActivity> vypsat další zprávu z fronty odpovědí.

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

GetNextReply<IActivity> vrátí hodnotu null, pokud ve frontě odpovědí nejsou žádné další zprávy.

Kontrolní aktivity

Kód v ukázce CoreBot používá Text pouze vlastnost vrácených aktivit. Ve složitějších robotech můžete chtít uplatnit další vlastnosti, jako je Speak, InputHintChannelData, atd.

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

Můžete to provést tak, že zkontrolujete každou vlastnost jednotlivě, jak je znázorněno výše, můžete napsat vlastní pomocné nástroje pro uplatnění aktivit nebo můžete použít jiné architektury, jako je FluentAssertions , k napsání vlastních kontrolních výrazů a zjednodušení testovacího kódu.

Předávání parametrů do dialogových oken

Konstruktor DialogTestClientinitialDialogOptions objekt, který lze použít k předání parametrů do dialogového okna. Například MainDialog v této ukázce inicializuje objekt z výsledků rozpoznávání jazyka, s entitami, které přeloží BookingDetails z promluvy uživatele, a předá tento objekt ve volání vyvolání BookingDialog.

Tento postup můžete implementovat v testu následujícím způsobem:

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 přijímá tento parametr a přistupuje k němu v testu stejným způsobem, jako by byl při vyvolání z MainDialog.

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

Výsledky odevzdání dialogového okna Asserting

Některá dialogová okna, jako BookingDialog je nebo DateResolverDialog vrátí hodnotu do volajícího dialogového okna. Objekt DialogTestClient zveřejňuje DialogTurnResult vlastnost, kterou lze použít k analýze a uplatnění výsledků vrácených dialogem.

Příklad:

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

Vlastnost DialogTurnResult lze také použít ke kontrole a uplatnění průběžných výsledků vrácených kroky v vodopádu.

Analýza výstupu testu

Někdy je potřeba přečíst přepis testu jednotek, abyste mohli analyzovat provádění testu, aniž byste museli test ladit.

Balíček Microsoft.Bot.Builder.Testing obsahuje XUnitDialogTestLogger protokol, který protokoluje zprávy odeslané a přijaté dialogem do konzoly.

Pokud chcete tento middleware použít, váš test musí vystavit konstruktor, který přijímá ITestOutputHelper objekt, který je poskytován spouštěčem testů XUnit, a vytvořit XUnitDialogTestLogger objekt, který se předá DialogTestClient prostřednictvím parametru middlewares .

public class BookingDialogTests
{
    private readonly IMiddleware[] _middlewares;

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

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

        ...
    }
}

Tady je příklad toho, co XUnitDialogTestLogger protokoly do výstupního okna při konfiguraci:

Example middleware output from XUnit.

Další informace o odesílání výstupu testu do konzoly při použití XUnit naleznete v tématu Zachycení výstupu v dokumentaci XUnit.

Tento výstup se také zaprotokoluje na buildovacím serveru během sestavení kontinuální integrace a pomůže vám analyzovat selhání sestavení.

Testy řízené daty

Ve většině případů se logika dialogového okna nemění a různé cesty provádění v konverzaci jsou založené na promluvách uživatele. Místo psaní jednoho testu jednotek pro každou variantu v konverzaci je jednodušší používat testy řízené daty (označované také jako parametrizovaný test).

Ukázkový test v části přehledu tohoto dokumentu například ukazuje, jak otestovat jeden tok spuštění, ale ne jiné, například:

  • Co se stane, když uživatel na potvrzení nehlásí?
  • Co když používají jiné datum?

Testy řízené daty nám umožňují testovat všechny tyto permutace bez nutnosti přepisovat testy.

V ukázce CoreBot používáme Theory testy z XUnit k parametrizaci testů.

Teorie testy využívající InlineData

Následující test zkontroluje, že se dialogové okno zruší, když uživatel oznámí "zrušit".

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

Pokud chcete dialogové okno zrušit, můžou uživatelé zadat "quit", "never mind" a "stop it". Místo psaní nového testovacího případu pro každé možné slovo napište jednu Theory testovací metodu, která přijímá parametry prostřednictvím seznamu InlineData hodnot pro definování parametrů pro každý testovací případ:

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

Nový test se spustí čtyřikrát s různými parametry a každý případ se v Průzkumníku ShouldBeAbleToCancel testů sady Visual Studio zobrazí jako podřízená položka. Pokud některý z nich selže, jak je znázorněno níže, můžete kliknout pravým tlačítkem myši a ladit scénář, který selhal, a nikoli znovu spustit celou sadu testů.

Example test results for in-line data.

Teorie testy s využitím MemberData a komplexních typů

InlineData je užitečný pro malé testy řízené daty, které přijímají parametry jednoduchého typu hodnoty (řetězec, int atd.).

BookingDetails Obdrží BookingDialog objekt a vrátí nový BookingDetails objekt. Neparametrizovaná verze testu pro tento dialog by vypadala takto:

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

Abychom tento test parametrizovali, vytvořili BookingDialogTestCase jsme třídu, která obsahuje naše data testovacího případu. Obsahuje počáteční BookingDetails objekt, očekávaný BookingDetails objekt a pole řetězců obsahující promluvy odeslané od uživatele a očekávané odpovědi z dialogového okna pro každé z nich.

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

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

    public BookingDetails ExpectedBookingDetails { get; set; }
}

Vytvořili jsme také pomocnou BookingDialogTestsDataGenerator třídu, která zveřejňuje metodu IEnumerable<object[]> BookingFlows() , která vrací kolekci testovacích případů, které bude test používat.

Aby bylo možné zobrazit každý testovací případ jako samostatnou položku v Průzkumníku testů sady Visual Studio, vyžaduje spouštěč testů XUnit, aby se zjednodušily komplexní typy, jako je BookingDialogTestCase implementace IXunitSerializable, bot.Builder.Testing framework poskytuje TestDataObject třídu, která implementuje toto rozhraní a lze ji použít k zabalení dat testovacích případů bez nutnosti implementovat IXunitSerializable.

Tady je fragment IEnumerable<object[]> BookingFlows() toho, jak se používají dvě třídy:

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

Jakmile vytvoříme objekt pro uložení testovacích dat a třídy, která zveřejňuje kolekci testovacích případů, použijeme atribut XUnit MemberData místo InlineData k předání dat do testu, první parametr pro MemberData je název statické funkce, která vrací kolekci testovacích případů a druhý parametr je typ třídy, která tuto metodu zveřejňuje.

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

Tady je příklad výsledků testů v Průzkumníku DialogFlowUseCases testů sady Visual Studio při spuštění testu:

Example results for the booking dialog.

Použití napodobení

Pro věci, které nejsou aktuálně testovány, můžete použít napodobené prvky. Pro referenci lze tuto úroveň obecně považovat za testování jednotek a integrace.

Napodobování tolik prvků, kolik vám umožní lépe izolovat část, kterou testujete. Mezi kandidáty na napodobení prvků patří úložiště, adaptér, middleware, kanál aktivit, kanály a cokoli jiného, co není přímo součástí robota. To může také zahrnovat dočasné odebrání určitých aspektů, jako je middleware, který není součástí části robota, kterého testujete, a izolovat jednotlivé části. Pokud ale testujete middleware, možná budete chtít robota napodobenit.

Napodobování prvků může mít několik formulářů, od nahrazení elementu jiným známým objektem až po implementaci minimálních funkcí hello world. To může mít také podobu odebrání prvku, pokud není nutné, nebo vynucení, aby nic nedělaly.

Napodobení umožňuje konfigurovat závislosti dialogového okna a zajistit, aby byly během provádění testu ve známém stavu, aniž by se musely spoléhat na externí prostředky, jako jsou databáze, jazykové modely nebo jiné objekty.

Aby bylo možné dialogové okno snadněji testovat a omezit jeho závislosti na externích objektech, budete možná muset vložit externí závislosti do konstruktoru dialogového okna.

Například místo vytvoření instance BookingDialog v MainDialog:

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

Jako parametr konstruktoru předáváme instanci BookingDialog :

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

To nám umožní nahradit BookingDialog instanci napodobeným objektem a zapisovat testy jednotek, aniž MainDialog by bylo nutné volat skutečnou BookingDialog třídu.

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

Dialogy napodobování

Jak je popsáno výše, MainDialog vyvolá se BookingDialog k získání objektu BookingDetails . Implementujeme a nakonfigurujeme napodobenou instanci BookingDialog následujícím způsobem:

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

V tomto příkladu jsme použili Moq k vytvoření napodobení dialogového okna a Setup metod konfigurace Returns jeho chování.

Napodobování výsledků služby LUIS

Poznámka:

Služba Language Understanding (LUIS) bude vyřazena 1. října 2025. Od 1. dubna 2023 nebudete moct vytvářet nové prostředky LUIS. Novější verze language understanding je teď dostupná jako součást jazyka Azure AI.

Konverzační jazyk understanding (CLU), funkce jazyka Azure AI, je aktualizovaná verze služby LUIS. Další informace o podpoře porozumění jazyku v sadě SDK služby Bot Framework najdete v tématu Principy přirozeného jazyka.

V jednoduchých scénářích můžete implementovat napodobení výsledků služby LUIS prostřednictvím kódu následujícím způsobem:

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

Výsledky služby LUIS můžou být složité. Pokud ano, je jednodušší zachytit požadovaný výsledek v souboru JSON, přidat ho jako prostředek do projektu a deserializovat ho do výsledku služby LUIS. Tady je příklad:

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

Další informace