Bot Builder v4 でのテスト : ダイアログから送れる各種メッセージに対応したテスト

今回はダイアログから送れる各種メッセージに対応したテストを見ていきます。メッセージの種類については [Bot Builder v4 でボット開発 : ダイアログから送れるメッセージの種類](https://qiita.com/kenakamu/items/eb3e7db2141525bd95ef) を参照してください。

# ソリューションの準備
ボットのコードは [Bot Builder v4 でボット開発 : ダイアログから送れるメッセージの種類](https://qiita.com/kenakamu/items/eb3e7db2141525bd95ef) で開発したものを使うので、コードの詳細はそちらの記事を参照してください。また前回の記事で開発した test-article10 のコードをベースに、article12 ブランチのコードをマージしてテストを開発します。

1\. 任意のフォルダでレポジトリをクローン。

```shell
git clone https://github.com/kenakamu/botbuilderv4completeguide/
cd botbuilderv4completeguide
```

2\. 以下のコマンドで article12 をチェックアウトした後、test-article10 をチェックアウトしてどちらもローカルにコピー。

```shell
git checkout article12
git checkout test-article10
```

3\. 以下コマンドで test-article11 ブランチを作成。

```shell
git checkout -b test-article11
```

4\. article12 のブランチをマージ。

```shell
git merge article12
```
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/52e0ce14-1f4a-9905-3e9d-825cdfa2d3fc.png)

5\. マージの競合があるため、以下コマンドで競合を確認。

```shell
git mergetool
```
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/292d6f11-5179-92cf-0d92-d838315baecf.png)


6\. マージのツールを起動しようとするので既定のまま Enter キーを押下。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/d8a5657d-037a-7632-ec0c-a30e12bde26d.png)

7\. 以下の様にマージ対象を選択して、「マージの許可」をクリック。

16 行目: Target
23 行目: Source と Target
26 行目: Target
138 行目: Source

8\. ルートフォルダ直下の Dialogs フォルダを myfirstbot フォルダに移動してから、コマンドプロンプトに戻り、変更を確認してから、コミットを実行。

```shell
git status
git add .
git commit -m "merge article12"
```
9\. myfirstbot.sln を起動してソリューションをビルド。マージした MyBot.cs のコンストラクタでエラーが出ることを確認。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/f93b6eb4-a283-e505-aa5b-2e19a8ead546.png)

10\. エラーとなっている行を削除して再度ビルド。その後既存のテストを全て実行。ウェルカムダイアログに変わったところだけエラーとなっていることを確認。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/6ca207d4-8491-32a3-bdf3-a8fd67a02824.png)

# テストの実装

今回は新規に追加されたウェルカムダイアログのテスト追加と、フローが変わった MyBot.cs のテスト修正をします。

## ウェルカムダイアログのユニットテスト

新しく追加されたウェルカムダイアログではリッチなメッセージが送信されてくるため、それに対応したテストを考えます。またウェルカムダイアログのフローの最後は、クライアント側には何も返さず EndDialogAsync が実行されるため、AssertReply が使えません。よって TestFlow 側で明示的に結果を返します。

1\. ユニットテストプロジェクトに WelcomeDialogUnitTest.cs を追加し、以下のコードを張り付け。

```csharp:WelcomeDialogUnitTest.cs
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Adapters;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Schema;
using Microsoft.Recognizers.Text;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Linq;
using System.Threading.Tasks;

namespace myfirstbot.unittest
{
    [TestClass]
    public class WelcomeDialogUnitTest
    {
    }
}
```

2\. ArrangeTest メソッドを追加。

- 呼び出したダイアログが完了した場合は、UserProfile の名前をクライアント側に返すように分岐を追加

```csharp
private (TestFlow testFlow, BotAdapter adapter, DialogSet dialogs) ArrangeTest()
{
    // ストレージとしてインメモリを利用
    IStorage dataStore = new MemoryStorage();
    // それぞれのステートを作成
    var conversationState = new ConversationState(dataStore);
    var userState = new UserState(dataStore);
    var accessors = new MyStateAccessors(userState, conversationState)
    {
        // DialogState を ConversationState のプロパティとして設定
        ConversationDialogState = conversationState.CreateProperty<DialogState>("DialogState"),
        // UserProfile を作成
        UserProfile = userState.CreateProperty<UserProfile>("UserProfile")
    };
    // テスト対象のダイアログをインスタンス化
    var dialogs = new DialogSet(accessors.ConversationDialogState);
    dialogs.Add(new WelcomeDialog(accessors));

    // アダプターを作成し必要なミドルウェアを追加
    var adapter = new TestAdapter()
        .Use(new SetLocaleMiddleware(Culture.Japanese))
        .Use(new AutoSaveStateMiddleware(userState, conversationState));

    // TestFlow の作成
    var testFlow = new TestFlow(adapter, async (turnContext, cancellationToken) =>
    {
        // ダイアログに必要なコードだけ追加
        var dialogContext = await dialogs.CreateContextAsync(turnContext, cancellationToken);

        var results = await dialogContext.ContinueDialogAsync(cancellationToken);
        if (results.Status == DialogTurnStatus.Empty)
        {
            await dialogContext.BeginDialogAsync(nameof(WelcomeDialog), null, cancellationToken);
        }
        // ダイアログが完了した場合は、UserProfile の名前をテスト側に返す
        else if (results.Status == DialogTurnStatus.Complete)
        {
            await turnContext.SendActivityAsync((await accessors.UserProfile.GetAsync(turnContext)).Name);
        }
    });

    return (testFlow, adapter, dialogs);
}
```

3\. プロファイルダイアログへ繋がるフローを検証するために、WelcomeDialog_ShouldGoToProfileDialog メソッドを追加。

- Reply の検証では、これまでの Text プロパティの確認ではなく、Attachment の確認となる
- 返ってくるリッチカードを全て検証

```csharp
[TestMethod]
public async Task WelcomeDialog_ShouldGoToProfileDialog()
{
    var arrange = ArrangeTest();
    // テストの追加と実行
    await arrange.testFlow
    .Send("foo")
    .AssertReply((activity) =>
    {
        // Activity にヒーローカードとアニメーションカードが含まれていることを確認。
        Assert.AreEqual((activity as Activity).Attachments.Count, 2);
        var heroCard = (activity as Activity).Attachments.First().Content as HeroCard;
        var animationCard = (activity as Activity).Attachments.Last().Content as AnimationCard;
        // ヒーローカードの内容を確認。
        Assert.AreEqual(heroCard.Title, "ようこそ My Bot へ！プロファイル登録をしますか？");
        Assert.AreEqual(heroCard.Buttons.Where(x=>x.Title == "はい").First().Value, "はい");
        Assert.AreEqual(heroCard.Buttons.Where(x=>x.Title == "スキップ").First().Value, "スキップ");
        Assert.AreEqual(heroCard.Buttons.Where(x=>x.Title == "Azure Bot Service").First().Value, "https://picsum.photos/300/200/?image=433");

        // アニメーションカードの内容を確認。
        Assert.AreEqual(animationCard.Title, "アニメーションサンプル");
        Assert.AreEqual(animationCard.Image.Url, "https://docs.microsoft.com/en-us/bot-framework/media/how-it-works/architecture-resize.png");
        Assert.AreEqual(animationCard.Media.Count, 1);
        Assert.AreEqual(animationCard.Media.First().Url, "http://i.giphy.com/Ki55RUbOV5njy.gif");
    })
    .Send("はい")
    .AssertReply((activity) =>
    {
        // Activity とアダプターからコンテキストを作成
        var turnContext = new TurnContext(arrange.adapter, activity as Activity);
        // ダイアログコンテキストを取得
        var dc = arrange.dialogs.CreateContextAsync(turnContext).Result;
        // 現在のダイアログスタックの一番上が ProfileDialog で その下が welcome であることを確認。
        var dialogInstances = (dc.Stack.Where(x => x.Id == nameof(WelcomeDialog)).First().State["dialogs"] as DialogState).DialogStack;
        Assert.AreEqual(dialogInstances[0].Id, nameof(ProfileDialog));
        Assert.AreEqual(dialogInstances[1].Id, "welcome");
    })
    .StartTestAsync();
}
```
4\. プロファイル登録をスキップした場合のフローを追加。

- 最後の AssertReply はダイアログから戻った値ではなく、TestFlow で設定したとおり UserProfile の Name が送られてくる

```csharp
[TestMethod]

public async Task WelcomeDialog_ShouldSetAnonymous()
{
    var arrange = ArrangeTest();
    // テストの追加と実行
    await arrange.testFlow
    .Send("foo")
    .AssertReply((activity) =>
    {
        // Activity にヒーローカードとアニメーションカードが含まれていることを確認。
        Assert.AreEqual((activity as Activity).Attachments.Count, 2);
        var heroCard = (activity as Activity).Attachments.First().Content as HeroCard;
        var animationCard = (activity as Activity).Attachments.Last().Content as AnimationCard;
        // ヒーローカードの内容を確認。
        Assert.AreEqual(heroCard.Title, "ようこそ My Bot へ！プロファイル登録をしますか？");
        Assert.AreEqual(heroCard.Buttons.Where(x => x.Title == "はい").First().Value, "はい");
        Assert.AreEqual(heroCard.Buttons.Where(x => x.Title == "スキップ").First().Value, "スキップ");
        Assert.AreEqual(heroCard.Buttons.Where(x => x.Title == "Azure Bot Service").First().Value, "https://picsum.photos/300/200/?image=433");

        // アニメーションカードの内容を確認。
        Assert.AreEqual(animationCard.Title, "アニメーションサンプル");
        Assert.AreEqual(animationCard.Image.Url, "https://docs.microsoft.com/en-us/bot-framework/media/how-it-works/architecture-resize.png");
        Assert.AreEqual(animationCard.Media.Count, 1);
        Assert.AreEqual(animationCard.Media.First().Url, "http://i.giphy.com/Ki55RUbOV5njy.gif");
    })
    .Send("スキップ")
    .AssertReply((activity) =>
    {
        // 返ってきたテキストが匿名かを確認
        Assert.AreEqual((activity as Activity).Text, "匿名");
    })
    .StartTestAsync();
}
```

5\. テストを実行して結果を確認。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/a4d2b785-b6e7-5a23-ed9d-431b89cbdea9.png)

## メインロジックの修正

実装の通り、ウェルカムダイアログに遷移することを確認します。

1\. MyBotUnitTest.cs の MyBot_ShouldGoToProfileDialogWithConversationUpdateWithoutUserProfile メソッドを以下の様に修正。

```csharp
[TestMethod]
public async Task MyBot_ShouldGoToProfileDialogWithConversationUpdateWithoutUserProfile()
{
    var arrange = ArrangeTest(false);

    var conversationUpdateActivity = new Activity(ActivityTypes.ConversationUpdate)
    {
        Id = "test",
        From = new ChannelAccount("TestUser", "Test User"),
        ChannelId = "UnitTest",
        ServiceUrl = "https://example.org",
        MembersAdded = new List<ChannelAccount>() { new ChannelAccount("TestUser", "Test User") }
    };

    // テストの追加と実行
    await arrange.testFlow
        .Send(conversationUpdateActivity)
        .AssertReply((activity) =>
        {
            // Activity とアダプターからコンテキストを作成
            var turnContext = new TurnContext(arrange.adapter, activity as Activity);
            // ダイアログコンテキストを取得
            var dc = arrange.dialogs.CreateContextAsync(turnContext).Result;
            // 現在のダイアログスタックの一番上が WelcomeDialog の checkStatus であることを確認。
            var dialogInstances = (dc.Stack.Where(x => x.Id == nameof(WelcomeDialog)).First().State["dialogs"] as DialogState).DialogStack;
            Assert.AreEqual(dialogInstances[0].Id, "checkStatus");
        })
        .StartTestAsync();
}
```

2\. テストを実行して結果を確認。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/3f03ad9f-d71e-1fb3-759b-5bc4e67f1298.png)

# まとめ

今回はリッチカードの検証と、ダイアログレベルでは応答を返さない場合のテストについて見ていきました。リッチメニューは確認する要素が多くなりがちですが必要に応じてテストを作ってください。

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

[この記事のサンプルコード](https://github.com/kenakamu/botbuilderv4completeguide/tree/test-article11)