Comment procéder à un test unitaire de robots
S'APPLIQUE À : SDK v4
Dans cette rubrique, nous vous expliquons comment :
- Créez des tests unitaires sur des bots.
- Utilisez l'assertion pour vérifier les activités retournées par une boîte de dialogue par rapport à des valeurs attendues.
- Utilisez l'assertion pour vérifier les résultats retournés par une boîte de dialogue.
- Créez différents types de tests pilotés par les données.
- Créez des objets fictifs pour les différentes dépendances d'un dialogue, telles que les dispositifs de reconnaissance de la langue, etc.
Prérequis
L'exemple Tests CoreBot utilisé dans cette rubrique fait référence au package Microsoft.Bot.Builder.Testing, XUnit et Moq pour créer des tests unitaires.
L'échantillon de bot principal utilise la compréhension du langage (LUIS) pour identifier les intentions utilisateur ; toutefois, l'identification de l'intention de l'utilisateur n'est pas le but de cet article. Pour plus d'informations sur l'identification des intentions utilisateur, consultez La compréhension du langage naturel et ajoutez la compréhension du langage naturel à votre bot.
Remarque
Compréhension du langage (LUIS) sera mis hors service le 1er octobre 2025. À compter du 1er avril 2023, vous ne pourrez pas créer de nouvelles ressources LUIS. Une version plus récente de Compréhension du langage est désormais disponible dans le cadre d'Azure AI Language.
Compréhension du langage conversationnel (CLU), une fonctionnalité d'Azure AI Language, est la version mise à jour de LUIS. Pour plus d'informations sur la prise en charge de compréhension du langage dans le kit de développement logiciel (SDK) Bot Framework, consultez Compréhension du langage naturel.
Tests des boîtes de dialogue
Dans l'échantillon CoreBot, les boîtes de dialogue sont testées par unité à partir de la classe DialogTestClient
qui fournit un mécanisme permettant de les tester de manière isolée en dehors d'un bot, sans avoir à déployer votre code sur un service Web.
À l’aide de cette classe, vous pouvez écrire des tests unitaires qui valident les réponses de boîtes de dialogue sur une base étape par étape. Les tests unitaires utilisant la classe DialogTestClient
doivent fonctionner avec d’autres boîtes de dialogue créées à l’aide de la bibliothèque de boîtes de dialogue botbuilder.
L’exemple suivant illustre les tests dérivés de 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 classe DialogTestClient
est définie dans l'espace de noms Microsoft.Bot.Builder.Testing
et incluse dans le package NuGet Microsoft.Bot.Builder.Testing.
DialogTestClient
Le premier paramètre de DialogTestClient
est le canal cible. Cela vous permet de tester différentes logiques de rendu en fonction de l'objectif du canal pour votre bot (Teams, Slack, etc.). Si vous n'êtes pas certain de votre canal cible, vous pouvez utiliser les ID de canal Emulator
ou Test
, mais gardez à l'esprit que certains composants peuvent se comporter différemment selon le canal actuel, par exemple, ConfirmPrompt
rend les options Oui/Non différemment pour les canaux Test
et Emulator
. Vous pouvez également utiliser ce paramètre pour tester la logique de rendu conditionnel de test dans votre boîte de dialogue en fonction de l’ID du canal.
Le deuxième paramètre est une instance du dialogue en cours de test. Dans l'exemple de code de cet article, sut
représente le système testé.
Le constructeur DialogTestClient
fournit des paramètres supplémentaires qui vous permettent de personnaliser davantage le comportement du client ou de passer des paramètres à la boîte de dialogue en cours de test, si nécessaire. Vous pouvez transmettre des données d’initialisation pour la boîte de dialogue, ajouter un intergiciel personnalisé ou utiliser vos propres TestAdapter et instance ConversationState
.
Envoi et réception des messages
La méthode SendActivityAsync<IActivity>
vous permet d’envoyer un énoncé de SMS ou une IActivity
à votre boîte de dialogue et retourne le premier message qu’il reçoit. Le paramètre <T>
est utilisé pour retourner une instance fortement typée de la réponse afin que vous puissiez la déclarer sans avoir à la caster.
var reply = await testClient.SendActivityAsync<IMessageActivity>("hi");
Assert.Equal("Where would you like to travel to?", reply.Text);
Dans certains scénarios, votre robot peut envoyer plusieurs messages en réponse à une seule activité. Dans ces cas, DialogTestClient
met en file d’attente les réponses et vous pouvez utiliser la méthode GetNextReply<IActivity>
pour dépiler le message suivant de la file de réponse.
reply = testClient.GetNextReply<IMessageActivity>();
Assert.Equal("All set, I have booked your flight to Seattle for tomorrow", reply.Text);
GetNextReply<IActivity>
retourne la valeur null s’il n’y a plus de messages dans la file de réponse.
Assertion des activités
Le code de l’exemple CoreBot déclare uniquement la propriété Text
des activités retournées. Dans les bots plus complexes, vous souhaiterez peut-être déclarer d'autres propriétés telles que 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);
Pour ce faire, vous pouvez vérifier chaque propriété individuellement comme indiqué ci-dessus, vous pouvez écrire vos propres utilitaires d’assistance pour déclarer des activités ou vous pouvez utiliser d'autres infrastructures comme FluentAssertions pour écrire des assertions personnalisées et simplifier votre code de test.
Transmission de paramètres vers votre boîte de dialogue
Le constructeur DialogTestClient
a un initialDialogOptions
qui peut être utilisé pour passer des paramètres à votre boîte de dialogue. Par exemple, le MainDialog
de cet échantillon initialise un objet BookingDetails
à partir des résultats de la reconnaissance linguistique, avec les entités qu'il résout à partir de l'énoncé de l'utilisateur, et transmet cet objet dans l'appel à l'invocation de BookingDialog
.
Vous pouvez implémenter cela dans un test comme suit :
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
reçoit ce paramètre et y accède dans le test de la même façon que lors de son appel à partir de MainDialog
.
private async Task<DialogTurnResult> DestinationStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
var bookingDetails = (BookingDetails)stepContext.Options;
...
}
L’assertion de la boîte de dialogue transformer les résultats
Certaines boîtes de dialogue telles que BookingDialog
ou DateResolverDialog
retournent une valeur à la boîte de dialogue appelante. L’objet DialogTestClient
expose une propriété DialogTurnResult
qui peut être utilisée pour analyser et déclarer les résultats retournés par la boîte de dialogue.
Par exemple :
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 propriété DialogTurnResult
peut également être utilisée pour inspecter et déclarer les résultats intermédiaires retournés par les étapes d’une cascade.
Analyse de la sortie de test
Il est parfois nécessaire de lire une transcription de test unitaire pour analyser l'exécution de test sans avoir à déboguer le test.
Le package Microsoft.Bot.Builder.Testing comprend un XUnitDialogTestLogger
qui enregistre les messages envoyés et reçus par la boîte de dialogue dans la console.
Pour utiliser cet intergiciel, votre test doit exposer un constructeur qui reçoit un objet ITestOutputHelper
fourni par le Test Runner XUnit et créer un XUnitDialogTestLogger
qui sera passé à DialogTestClient
par le biais du paramètre 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);
...
}
}
Voici un exemple de la façon dont XUnitDialogTestLogger
se connecte à la fenêtre de sortie lorsqu'il est configuré :
Pour plus d’informations sur l’envoi d’une sortie de test à la console lors de l’utilisation de XUnit, consultez Capture de la sortie dans la documentation XUnit.
Cette sortie sera également consignée sur le serveur de builds pendant que l’intégration continue génère et vous aide à analyser les échecs de build.
Tests pilotés par les données
Dans la plupart des cas, la logique de la boîte de dialogue ne change pas et les différents chemins d’exécution d’une conversation sont basés sur les énoncés utilisateur. Plutôt que d'écrire un seul test unitaire pour chaque variante de la conversation, il est plus facile d'utiliser des tests pilotés par les données (également appelés tests paramétrables).
Par exemple, l'échantillon de test dans la section de vue d'ensemble de ce document montre comment tester un flux d'exécution, mais pas d'autres, tels que :
- Que se passe-t-il si l'utilisateur dit non à la confirmation ?
- Que se passe-t-il s'ils utilisent une date différente ?
Les tests pilotés par les données nous permettent de tester toutes ces permutations sans avoir à réécrire les tests.
Dans l’exemple CoreBot, nous utilisons des tests Theory
de XUnit pour paramétrer des tests.
Tests de théorie utilisant InlineData
Le test suivant vérifie qu'une boîte de dialogue est annulée lorsque l'utilisateur dit « annuler ».
[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);
}
Pour annuler une boîte de dialogue, les utilisateurs peuvent saisir « quitter », « ignorer » et « arrêter ». Au lieu d'écrire un nouveau cas de test pour chaque mot possible, écrivez une méthode de test Theory
unique qui accepte des paramètres via une liste de valeurs InlineData
pour définir les paramètres de chaque cas de test :
[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);
}
Le nouveau test sera exécuté quatre fois avec les différents paramètres et chaque cas sera affiché en tant qu'élément enfant sous le test ShouldBeAbleToCancel
dans l'Explorateur de tests Visual Studio. Si l'un d'eux échoue, comme indiqué ci-dessous, vous pouvez cliquer avec le bouton de droite et déboguer le scénario qui a échoué au lieu de exécute à nouveau l'ensemble complet des tests.
Tests de théorie utilisant des types MemberData et complexes
InlineData
est utile pour les tests pilotés par les données de petite taille qui reçoivent des paramètres de type valeur simples (chaîne, int, etc.).
La BookingDetails
reçoit un objet BookingDialog
et retourne un nouvel objet BookingDetails
. Une version non paramétrable d’un test pour cette boîte de dialogue doit se présenter comme suit :
[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);
}
Pour paramétrer ce test, nous avons créé une classe BookingDialogTestCase
qui contient les données de notre cas de test. Il contient l’objet BookingDetails
initial, l’objet BookingDetails
attendu et un tableau de chaînes contenant les énoncés envoyés par l’utilisateur et les réponses attendues de la boîte de dialogue pour chaque tour.
public class BookingDialogTestCase
{
public BookingDetails InitialBookingDetails { get; set; }
public string[,] UtterancesAndReplies { get; set; }
public BookingDetails ExpectedBookingDetails { get; set; }
}
Nous avons également créé une classe BookingDialogTestsDataGenerator
d’assistance exposant une méthode IEnumerable<object[]> BookingFlows()
qui retourne une collection des cas de test à utiliser par le test.
Pour afficher chaque cas de test sous la forme d’un élément distinct dans l’Explorateur de tests Visual Studio, le Test Runner XUnit requiert que les types complexes tels que BookingDialogTestCase
implémentent IXunitSerializable
, pour simplifier cela, l'infrastructure Bot.Builder.Testing fournit un classe TestDataObject
qui implémente cette interface et qui peut être utilisée pour inclure dans un wrapper les données de cas de test sans avoir à implémenter IXunitSerializable
.
Voici un fragment de IEnumerable<object[]> BookingFlows()
qui montre comment les deux classes sont utilisées :
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) };
}
}
Une fois que nous avons créé un objet pour stocker les données de test et une classe qui expose une collection de cas de test, nous utilisons l’attribut MemberData
de XUnit au lieu de InlineData
pour alimenter les données dans le test, le premier paramètre pour MemberData
est le nom de la fonction statique qui retourne la collection de cas de test et le deuxième paramètre est le type de classe qui expose cette méthode.
[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);
}
Voici un exemple des résultats des tests DialogFlowUseCases
dans l'Explorateur de tests Visual Studio lorsque le test est exécuté :
Utilisation des objets fictifs
Vous pouvez utiliser des éléments fictifs pour tout ce qui n'est pas en cours de test. À titre de référence, ce niveau peut généralement être assimilé à un test d’unité et d’intégration.
En simulant autant d'éléments que vous le souhaitez, vous pouvez mieux isoler la partie à tester. Les éléments fictifs possibles sont le stockage, l'adaptateur, l'intergiciel, le pipeline d'activités, les canaux et tout ce qui ne fait pas directement partie de votre bot. Il est également possible d'impliquer la suppression temporaire de certains éléments, par exemple un intergiciel non concerné par ce que vous testez dans votre bot, et ce, afin d'isoler chaque partie. Si vous testez votre intergiciel, vous pouvez cependant vouloir simuler votre bot à la place.
La simulation d'éléments peut prendre une multitude de formes, depuis le remplacement d'un élément par un autre objet connu jusqu'à la mise en œuvre d'une fonctionnalité minimale de type « hello world ». L'élément peut aussi être retiré s'il n'est pas nécessaire ou s'il est forcé de rester inactif.
Les objets fictifs nous permettent de configurer les dépendances d'une boîte de dialogue et de s'assurer qu'elles sont dans un état connu pendant l'exécution du test sans avoir à s'appuyer sur des ressources externes comme des bases de données, des modèles de langage ou d'autres objets.
Pour faciliter le test de votre boîte de dialogue et réduire ses dépendances sur des objets externes, vous devrez peut-être injecter les dépendances externes dans le constructeur de boîte de dialogue.
Par exemple, au lieu d'instancier BookingDialog
dans MainDialog
:
public MainDialog()
: base(nameof(MainDialog))
{
...
AddDialog(new BookingDialog());
...
}
Nous passons une instance de BookingDialog
en tant que paramètre de constructeur :
public MainDialog(BookingDialog bookingDialog)
: base(nameof(MainDialog))
{
...
AddDialog(bookingDialog);
...
}
Cela nous permet de remplacer l'instance BookingDialog
par un objet fictif et d’écrire des tests unitaires pour la MainDialog
sans avoir à appeler la classe BookingDialog
réelle.
// 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);
Simulation de boîtes de dialogue
Comme décrit ci-dessus, MainDialog
appelle BookingDialog
pour obtenir l’objet BookingDetails
. Nous implémentons et configurons une instance fictive de BookingDialog
comme suit :
// 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);
Dans cet exemple, nous avons utilisé Moq pour créer la boîte de dialogue fictive et les méthodes Setup
et Returns
pour configurer son comportement.
Résultats de la simulation LUIS
Remarque
Compréhension du langage (LUIS) sera mis hors service le 1er octobre 2025. À compter du 1er avril 2023, vous ne pourrez pas créer de nouvelles ressources LUIS. Une version plus récente de Compréhension du langage est désormais disponible dans le cadre d'Azure AI Language.
Compréhension du langage conversationnel (CLU), une fonctionnalité d'Azure AI Language, est la version mise à jour de LUIS. Pour plus d'informations sur la prise en charge de compréhension du langage dans le kit de développement logiciel (SDK) Bot Framework, consultez Compréhension du langage naturel.
Dans des scénarios simples, vous pouvez implémenter des résultats LUIS fictifs à l’aide du code comme suit :
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);
});
Les résultats LUIS peuvent être complexes. Lorsque c'est le cas, il est plus simple de capturer le résultat souhaité dans un fichier JSON, de l'ajouter en tant que ressource à votre projet et de le désérialiser en un résultat LUIS. Voici un exemple :
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);
});