Bot Builder v4.5 のユニットテスト : ダイアログクラスのテスト - BookingDialog

[前回](https://qiita.com/kenakamu/items/475e1d347d8317d815d1) は MainDialog のテストを見ていきましたが、今回は BookingDialog のテストを見ていきます。

# BookingDialog 

このダイアログでは、主に以下の処理を行います。

- LUIS の結果がある場合は、各ステップはスキップ
- LUIS の結果が不足している場合
    - 行先の確認
    - 出発元の確認
    - 日付の確認
    - 最終確認

では早速テストを見ていきます。

# BookingDialogTests コンストラクタ

BookingDialogTests クラス内で実行するテストに共通するものをコンストラクタで準備します。

```csharp
private readonly IMiddleware[] _middlewares;

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

# DialogFlowUseCases テスト

このテストでは LUIS の結果がどこまで取得できているかによって、それぞれをテストします。

```csharp:BookingDialogTests.cs
[Theory]
[MemberData(nameof(BookingDialogTestsDataGenerator.BookingFlows), MemberType = typeof(BookingDialogTestsDataGenerator))]
public async Task DialogFlowUseCases(TestDataObject testData)
{
    // Arrange
    var bookingTestData = testData.GetObject<BookingDialogTestCase>();
    var sut = new BookingDialog();
    var testClient = new DialogTestClient(Channels.Test, sut, bookingTestData.InitialBookingDetails, _middlewares);

    // Execute the test case
    Output.WriteLine($"Test Case: {bookingTestData.Name}");
    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);
    }

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

## テストパターンの作成

今回も Theory テストですが、 MemberData 属性を使ってテストを取得しています。MemberData 属性では名前とそれぞれのテストをクラスで指定できます。

### BookingDialogTestsDataGenerator クラス

テストのパターンは DialogFlowUseCases メソッドの 引数で指定されている TestDataObject として返されます。BookingDialogTestsDataGenerator の BuildTestCaseObject メソッドでテスト用のデータを作成しています。

```csharp:BookingDialogTestsDataGenerator.cs
private static object[] BuildTestCaseObject(string testCaseName, BookingDetails inputBookingInfo, string[,] utterancesAndReplies, BookingDetails expectedBookingInfo)
{
    var testData = new BookingDialogTestCase()
    {
        Name = testCaseName,
        InitialBookingDetails = inputBookingInfo,
        UtterancesAndReplies = utterancesAndReplies,
        ExpectedBookingDetails = expectedBookingInfo,
    };
    return new object[] { new TestDataObject(testData) };
}
```
また MemberData で指定されている BookingFlows メソッドにて、複数のテストオブジェクトを作成しています。

```csharp
public static IEnumerable<object[]> BookingFlows()
{
    yield return BuildTestCaseObject(
        "Full flow",
        new BookingDetails(),
        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 },
        },
        new BookingDetails
        {
            Destination = "Seattle",
            Origin = "New York",
            TravelDate = $"{DateTime.Now.AddDays(1):yyyy-MM-dd}",
        });
//以下省略
```

初めのテストケースは以下のようになっています。

**名前**
"Full flow"
**LUIS の結果から作成された BookingDetail**
BookingDetails()
**会話**
{ "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 },
**会話の結果作成された BookingDetail**
new BookingDetails
{
    Destination = "Seattle",
    Origin = "New York",
    TravelDate = $"{DateTime.Now.AddDays(1):yyyy-MM-dd}",
});

これは LUIS で必要な情報が何も取得できなかった場合のフローであることが分かります。
同様に他のテストケースも確認してください。

## テストの実行と結果の確認

テストデータと DialogTestClient を使ってテストを実行します。まずはテストデータから DialogTestClient を初期化します。

```csharp
// Arrange
var bookingTestData = testData.GetObject<BookingDialogTestCase>();
var sut = new BookingDialog();
var testClient = new DialogTestClient(Channels.Test, sut, bookingTestData.InitialBookingDetails, _middlewares);
```

テストデータの指定された会話を順に実行していきます。

```csharp
// Execute the test case
Output.WriteLine($"Test Case: {bookingTestData.Name}");
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);
}
```

すべての会話が終了後に取得できる BookingDetail で結果を確認します。

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

テストエクスプローラーから結果を確認すると、テストデータとして用意したものそれぞれの結果が出ます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/635588bc-9c63-0322-a925-1d130aefb7b8.png)


# ShouldBeAbleToCancelAtAnyTime テスト

ダイアログはどのタイミングでもキャンセルできるよう開発されていて、このテストではキャンセルが意図通り行えるか確認します。

```csharp:BookingDialogTests.cs
[Theory]
[MemberData(nameof(BookingDialogTestsDataGenerator.CancelFlows), MemberType = typeof(BookingDialogTestsDataGenerator))]
public async Task ShouldBeAbleToCancelAtAnyTime(TestDataObject testData)
{
    // Arrange
    var bookingTestData = testData.GetObject<BookingDialogTestCase>();
    var sut = new BookingDialog();
    var testClient = new DialogTestClient(Channels.Test, sut, bookingTestData.InitialBookingDetails, _middlewares);

    // Execute the test case
    Output.WriteLine($"Test Case: {bookingTestData.Name}");
    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.Equal(DialogTurnStatus.Complete, testClient.DialogTurnResult.Status);
    Assert.Null(testClient.DialogTurnResult.Result);
}
```

やり方は上記のテストと全く同じでパターンが異なるだけです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/3382511e-a819-2650-860c-ec13f119d604.png)

# まとめ

今回はより複雑なテストケースの作成方法として MemberData を使う方法を見ていきました。これは非常に強力であるとともに、テストケースを別管理できる柔軟な方法で、是非マスターしたいです。次回は CancelAndHelpDialog のテストを見ていきます。

[次の記事へ](https://qiita.com/kenakamu/items/471a0479d75c57914fb8)
[目次へ戻る](https://qiita.com/kenakamu/items/6dc043cfc1f199032883)