Bot Builder v4 でのテスト : 多言語対応したアダプティブカードのユニットテスト

今回は多言語対応したアダプティブカードのユニットテストを見ていきます。アダプティブカードの多言語化については [Bot Builder v4 でボット開発 : 多言語対応を行う - アダプティブカードの多言語化](https://qiita.com/kenakamu/items/b6eeb61560d4bd9a047a) を参照してください。

# ソリューションの準備
ボットのコードは [Bot Builder v4 でボット開発 : 多言語対応を行う - アダプティブカードの多言語化](https://qiita.com/kenakamu/items/b6eeb61560d4bd9a047a) で開発したものを使うので、コードの詳細はそちらの記事を参照してください。また前回の記事で開発した test-article13 のコードをベースに、article16 ブランチのコードをマージしてテストを開発します。

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

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

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

```shell
git checkout article16
git checkout test-article13
```

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

```shell
git checkout -b test-article14
```

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

```shell
git merge article16
```
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/36c1284c-dfc6-e6a8-a6c1-9722868f358e.png)

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

```shell
git mergetool
```

6\. JSON ファイルについては ja-JP フォルダおよび en-US フォルダの JSON は保持、他は削除を選択。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/11d37484-73a2-520b-a3fb-beded816a47b.png)

7\. マージ対象のファイルについてはツールを起動しようとするので既定のまま Enter キーを押下。ソースの変更を採用。マージ完了後、AdaptiveJsons、Resources フォルダを myfirstbot フォルダへ移動。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/f8fc9c5b-8d90-3bef-70cf-cbf85219debb.png)

8\. ソリューションを Visual Studio で開いてビルド実行。まず myfirstbot プロジェクトでマージの影響で graphClient が見つからないエラーが多く出ることを確認。エラーが解消するようにクラスプロパティに MSGraphService を追加し、コンストラクタでインスタンスを取得するよう対応。

```csharp:PhotoUpdateDialog.csの場合
public class PhotoUpdateDialog : ComponentDialog
{
    private IServiceProvider serviceProvider;
    private IStringLocalizer<PhotoUpdateDialog> localizer;
    private MSGraphService graphClient;

    public PhotoUpdateDialog(IServiceProvider serviceProvider, IStringLocalizer<PhotoUpdateDialog> localizer) : base(nameof(PhotoUpdateDialog))
    {
        this.serviceProvider = serviceProvider;
        this.localizer = localizer;
        this.graphClient = (MSGraphService)serviceProvider.GetService(typeof(MSGraphService));
...
```

```csharp:ScheduleDialog.csの場合
public class ScheduleDialog : ComponentDialog
{
    private IStringLocalizer<ScheduleDialog> localizer;
    private MSGraphService graphClient;

    public ScheduleDialog(IServiceProvider serviceProvider, IStringLocalizer<ScheduleDialog> localizer) : base(nameof(ScheduleDialog))
    {
        this.localizer = localizer;
        this.graphClient = (MSGraphService)serviceProvider.GetService(typeof(MSGraphService));
...
```
9\. myfirstbot プロジェクトだけビルドしてエラーが出ないことを確認。

# アダプティブカードのユニットテスト

アダプティブカードのテストは基本的にリソース使った多言語対応のユニットテストと同じですが、以下の点が異なります。

- アダプティブカードのパスに言語 ID が含まれる
- 一部の文字列はリソースファイルにはなく、アダプティブカードで直接定義されている

リソースファイルから取得できない文字列を使う場合は、DataRow に複数のパラメーターを渡すことでテストを行います。

## ヘルパーファイルの追加とリンクファイルの更新

今回複数のユニットテストで StringLocalizer<T> が必要になるため、ヘルパーコードを作ります。また今回ほぼすべてのテストで UserProfile を初期設定した Accessors を使うため、こちらも共通化します。

1\. ユニットテストプロジェクトの Helpers フォルダに StringLocalizerFactory.cs を追加。以下のコードに差し替え。

```csharp
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;

namespace myfirstbot.unittest.Helpers
{
    public class StringLocalizerFactory
    {
        static public StringLocalizer<T> GetStringLocalizer<T>()
        {
            ResourceManagerStringLocalizerFactory factory = new ResourceManagerStringLocalizerFactory(
                Options.Create(new LocalizationOptions() { ResourcesPath = "Resources" }), NullLoggerFactory.Instance);
            var localizer = new StringLocalizer<T>(factory);
            return localizer;
        }
    }
}
```

2\. 同様に AccessorsFactory.cs を追加して、以下のコードと差し替え。

```csharp:AccessorsFactory.cs
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
using Moq;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace myfirstbot.unittest.Helpers
{
    public  class AccessorsFactory
    {
        static public MyStateAccessors GetAccessors(string language, bool returnUserProfile = true)
        {
            // ストレージとしてインメモリを利用
            IStorage dataStore = new MemoryStorage();
            // それぞれのステートを作成
            var mockStorage = new Mock<IStorage>();
            // User1用に返すデータを作成
            // UserState のキーは <channelId>/users/<userId>
            var dictionary = new Dictionary<string, object>();
            // ユーザープロファイルを設定
            if (returnUserProfile)
            {
                dictionary.Add("test/users/user1", new Dictionary<string, object>()
                {
                    { "UserProfile", new UserProfile() { Name = "Ken", Age = 0, Language = language } }
                });
            }
            // ストレージへの読み書きを設定
            mockStorage.Setup(ms => ms.WriteAsync(It.IsAny<Dictionary<string, object>>(), It.IsAny<CancellationToken>()))
                .Returns((Dictionary<string, object> dic, CancellationToken token) =>
                {
                    foreach (var dicItem in dic)
                    {
                        if (dicItem.Key != "test/users/user1")
                        {
                            if (dictionary.ContainsKey(dicItem.Key))
                            {
                                dictionary[dicItem.Key] = dicItem.Value;
                            }
                            else
                            {
                                dictionary.Add(dicItem.Key, dicItem.Value);
                            }
                        }
                    }

                    return Task.CompletedTask;
                });
            mockStorage.Setup(ms => ms.ReadAsync(It.IsAny<string[]>(), It.IsAny<CancellationToken>()))
                .Returns(() =>
                {
                    return Task.FromResult(result: (IDictionary<string, object>)dictionary);
                });

            // それぞれのステートを作成
            var conversationState = new ConversationState(mockStorage.Object);
            var userState = new UserState(mockStorage.Object);
            var accessors = new MyStateAccessors(userState, conversationState)
            {
                // DialogState を ConversationState のプロパティとして設定
                ConversationDialogState = conversationState.CreateProperty<DialogState>("DialogState"),
                // UserProfile を作成
                UserProfile = userState.CreateProperty<UserProfile>("UserProfile")
            };

            return accessors;
        }
    }
}
```

3\. myfirstbot.unittest プロジェクトを右クリックして「myfirstbot.unittest.csproj の編集」をクリック。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/97b9faa9-d466-6d8f-ec90-f99a7db9fc92.png)

4\. AdaptiveJsons と Resources の ItemGroup を以下の様に更新

```xml
<ItemGroup>
  <Content Include="..\myfirstbot\AdaptiveJsons\*\*.json">
    <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    <Link>AdaptiveJsons\%(RecursiveDir)\%(FileName)%(Extension)</Link>
  </Content>
</ItemGroup>
<ItemGroup>
  <Content Include="..\myfirstbot\Resources\*.resx">
    <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    <Link>Resources\%(FileName)%(Extension)</Link>
  </Content>
</ItemGroup>
```

## WelcomeDialogUnitTest の更新

1\. すでに StringLocalizer を使っている WelcomeDialogUnitTest.cs の ArrangeTest メソッドを以下のコードに書き換え。

- StringLocalizerFactory から StringLocalizer を取得
- Accessors をファクトリーから取得

```csharp
private (TestFlow testFlow, BotAdapter adapter, DialogSet dialogs, StringLocalizer<WelcomeDialog> localizer) ArrangeTest(string language)
{
    var accessors = AccessorsFactory.GetAccessors(language);

    // リソースを利用するため StringLocalizer を作成
    var localizer = StringLocalizerFactory.GetStringLocalizer<WelcomeDialog>();

    // IServiceProvider のモック
    var serviceProvider = new Mock<IServiceProvider>();

    // WelcomeDialog クラスで解決すべきサービスを登録
    serviceProvider.Setup(x => x.GetService(typeof(ProfileDialog))).Returns(new ProfileDialog(accessors, null));
    serviceProvider.Setup(x => x.GetService(typeof(SelectLanguageDialog))).Returns(new SelectLanguageDialog(accessors));

    // テスト対象のダイアログをインスタンス化
    var dialogs = new DialogSet(accessors.ConversationDialogState);
    dialogs.Add(new WelcomeDialog(accessors, localizer, serviceProvider.Object));

    // アダプターを作成し必要なミドルウェアを追加
    var adapter = new TestAdapter()
        .Use(new AutoSaveStateMiddleware(accessors.UserState, accessors.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, localizer);
}
```

2\. 必要に応じて using を追加。

## MenuDialogUnitTest の更新

1\. MenuDialogUnitTest.cs の ArrangeTest メソッドを以下と差し替え

- StringLocalizerFactory で StringLocalizer を取得
- 戻り値のタプルに StringLocaliser<MenuDialog> を追加
- UserProfile で引数より言語を指定
- Accessors をファクトリーより取得

```csharp
private (TestFlow testFlow, BotAdapter adapter, DialogSet dialogs, StringLocalizer<MenuDialog> localizer) ArrangeTest(string language)
{
    var accessors = AccessorsFactory.GetAccessors(language);

    // リソースを利用するため StringLocalizer を作成
    var localizer = StringLocalizerFactory.GetStringLocalizer<MenuDialog>();

    // IServiceProvider のモック
    var serviceProvider = new Mock<IServiceProvider>();

    // MenuDialog クラスで解決すべきサービスを登録
    serviceProvider.Setup(x => x.GetService(typeof(LoginDialog))).Returns(new LoginDialog(StringLocalizerFactory.GetStringLocalizer<LoginDialog>()));
    serviceProvider.Setup(x => x.GetService(typeof(WeatherDialog))).Returns(new WeatherDialog(accessors, StringLocalizerFactory.GetStringLocalizer<WeatherDialog>()));
    serviceProvider.Setup(x => x.GetService(typeof(ScheduleDialog))).Returns(new ScheduleDialog(serviceProvider.Object, StringLocalizerFactory.GetStringLocalizer<ScheduleDialog>()));

    // テスト対象のダイアログをインスタンス化
    var dialogs = new DialogSet(accessors.ConversationDialogState);
    dialogs.Add(new MenuDialog(serviceProvider.Object, localizer));

    // アダプターを作成し必要なミドルウェアを追加
    var adapter = new TestAdapter()
        .Use(new SetLocaleMiddleware(Culture.Japanese))
        .Use(new AutoSaveStateMiddleware(accessors.UserState, accessors.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(MenuDialog), null, cancellationToken);
        }
    });

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

2\. MenuDialog_ShouldGoToWeatherDialog メソッドを以下の様に変更。

- DataRow で全ての言語をテスト
- Choice の戻り値は言語によって多少異なるため、キーワードのみ確認

```csharp
[TestMethod]
[DataRow("ja-JP")]
[DataRow("en-US")]
public async Task MenuDialog_ShouldGoToWeatherDialog(string language)
{
    // 言語を指定してテストを作成
    var arrange = ArrangeTest(language);
    Thread.CurrentThread.CurrentCulture = new CultureInfo(language);
    Thread.CurrentThread.CurrentUICulture = new CultureInfo(language);

    // テストの追加と実行
    await arrange.testFlow
    .Send("foo")
    .AssertReply((activity) =>
    {
        Assert.IsTrue((activity as Activity).Text.IndexOf(arrange.localizer["choicemenu"]) >= 0);
        Assert.IsTrue((activity as Activity).Text.IndexOf(arrange.localizer["checkweather"]) >= 0);
        Assert.IsTrue((activity as Activity).Text.IndexOf(arrange.localizer["checkschedule"]) >= 0);
    })
    .Send(arrange.localizer["checkweather"])
    .AssertReply((activity) =>
    {
    // Activity とアダプターからコンテキストを作成
    var turnContext = new TurnContext(arrange.adapter, activity as Activity);
    // ダイアログコンテキストを取得
    var dc = arrange.dialogs.CreateContextAsync(turnContext).Result;
    // 現在のダイアログスタックの一番上が WeatherDialog で その下が MenuDialog であることを確認。
    var dialogInstances = (dc.Stack.Where(x => x.Id == nameof(MenuDialog)).First().State["dialogs"] as DialogState).DialogStack;
        Assert.AreEqual(dialogInstances[0].Id, nameof(WeatherDialog));
    })
    .StartTestAsync();
}
```
3\. MenuDialog_ShouldGoToScheduleDialog メソッドを以下の様に変更。

- DataRow で全ての言語をテスト
- Choice の戻り値は言語によって多少異なるため、キーワードのみ確認

```csharp
[TestMethod]
[DataRow("ja-JP")]
[DataRow("en-US")]
[ExpectedException(typeof(AggregateException), "OAuthPrompt.GetUserToken(): not supported by the current adapter")]

public async Task MenuDialog_ShouldGoToScheduleDialog(string language)
{
    // 言語を指定してテストを作成
    var arrange = ArrangeTest(language);
    Thread.CurrentThread.CurrentCulture = new CultureInfo(language);
    Thread.CurrentThread.CurrentUICulture = new CultureInfo(language);

    await arrange.testFlow
    .Send("foo")
    .AssertReply((activity) =>
    {
        Assert.IsTrue((activity as Activity).Text.IndexOf(arrange.localizer["choicemenu"]) >= 0);
        Assert.IsTrue((activity as Activity).Text.IndexOf(arrange.localizer["checkweather"]) >= 0);
        Assert.IsTrue((activity as Activity).Text.IndexOf(arrange.localizer["checkschedule"]) >= 0);
    })
    // 予定を確認を送った時点で OAuthPrompt.GetUserToken(): not supported by the current adapter エラーが出る
    .Test(arrange.localizer["checkschedule"], "dummy")
    .StartTestAsync();            
}
```

4\. 必要に応じて using を追加。

## WeatherDialogUnitTest の更新

WeatherDialog ではアダプティブカード内に翻訳された文字列があり、また戻り値としても利用します。そこで DataRow の複数パラメーターで対応します。

1\. WeatherDialogUnitTest.cs の ArrangeTest を以下のコードと差し替え。

- StringLocalizerFactory で StringLocalizer を取得
- 戻り値のタプルに StringLocaliser<MenuDialog> を追加
- UserProfile で引数より言語を指定
- Accessors をファクトリーより取得

```csharp:WeatherDialogUnitTest.cs
private (TestFlow testFlow, StringLocalizer<WeatherDialog> localizer) ArrangeTest(string language)
{
    var accessors = AccessorsFactory.GetAccessors(language);

    // リソースを利用するため StringLocalizer を作成
    var localizer = StringLocalizerFactory.GetStringLocalizer<WeatherDialog>();

    // テスト対象のダイアログをインスタンス化
    var dialogs = new DialogSet(accessors.ConversationDialogState);
    dialogs.Add(new WeatherDialog(accessors, localizer));

    // アダプターを作成し必要なミドルウェアを追加
    var adapter = new TestAdapter()
        .Use(new AutoSaveStateMiddleware(accessors.UserState, accessors.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(WeatherDialog), null, cancellationToken);
        }
        else if (results.Status == DialogTurnStatus.Complete)
        {
            await turnContext.SendActivityAsync("Done");
        }
    });

    return (testFlow, localizer);
}
```

2\. WeatherDialog_ShouldReturnChoice メソッドを以下のコードと差し替え。

- DataRow に複数の値を渡して言語と日の違いを両方テスト

```csharp
[TestMethod]
[DataRow("ja-JP","明日")]
[DataRow("ja-JP","明後日")]
[DataRow("en-US","tomorrow")]
[DataRow("en-US","day after tomorrow")]
public async Task WeatherDialog_ShouldReturnChoice(string language, string date)
{
    // 言語を指定してテストを作成
    var arrange = ArrangeTest(language);
    Thread.CurrentThread.CurrentCulture = new CultureInfo(language);
    Thread.CurrentThread.CurrentUICulture = new CultureInfo(language);

    await arrange.testFlow
    .Send("foo")
    .AssertReply((activity) =>
    {
        // アダプティブカードを比較
        Assert.AreEqual(
            JObject.Parse((activity as Activity).Attachments[0].Content.ToString()).ToString(),
            JObject.Parse(File.ReadAllText($"./AdaptiveJsons/{language}/Weather.json").Replace("{0}", arrange.localizer["today"])).ToString()
        );
    })
    .Send("他の日の天気")
    .AssertReply((activity) =>
    {
        // アダプティブカードを比較
        Assert.AreEqual(
            JObject.Parse((activity as Activity).Attachments[0].Content.ToString()).ToString(),
            JObject.Parse(File.ReadAllText($"./AdaptiveJsons/{language}/WeatherDateChoice.json")).ToString()
        );
    })
    .Send(date)
    .AssertReply((activity) =>
    {
        // アダプティブカードを比較
        Assert.AreEqual(
            JObject.Parse((activity as Activity).Attachments[0].Content.ToString()).ToString(),
            JObject.Parse(File.ReadAllText($"./AdaptiveJsons/{language}/Weather.json").Replace("{0}", date)).ToString()
        );
    })
    .Test(arrange.localizer["end"], "Done")
    .StartTestAsync();
}
```

3\. WeatherDialog_ShouldReturnChoiceAndComplete メソッドを以下のコードに差し替え。

```csharp
[TestMethod]
[DataRow("ja-JP")]
[DataRow("en-US")]
public async Task WeatherDialog_ShouldReturnChoiceAndComplete(string language)
{
    // 言語を指定してテストを作成
    var arrange = ArrangeTest(language);
    Thread.CurrentThread.CurrentCulture = new CultureInfo(language);
    Thread.CurrentThread.CurrentUICulture = new CultureInfo(language);

    await arrange.testFlow
    .Send("foo")
    .AssertReply((activity) =>
    {
        // アダプティブカードを比較
        Assert.AreEqual(
            JObject.Parse((activity as Activity).Attachments[0].Content.ToString()).ToString(),
            JObject.Parse(File.ReadAllText($"./AdaptiveJsons/{language}/Weather.json").Replace("{0}", arrange.localizer["today"])).ToString()
        );
    })
    .Test(arrange.localizer["end"], "Done")
    .StartTestAsync();
}
```

4\. 必要に応じて using を追加。

## 他テストの更新

他も基本同じ方向で改修します。

1\. PhotoUpdateDialogUnitTest.cs を以下のコードと差し替えます。

- 言語を指定したテストのアレンジ
- DataRow を利用したテスト

```csharp:PhotoUpdateDialogUnitTest.cs 
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Adapters;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Schema;
using Microsoft.Extensions.Localization;
using Microsoft.Graph;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using myfirstbot.unittest.Helpers;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace myfirstbot.unittest
{
    [TestClass]
    public class PhotoUpdateDialogUnitTest
    {
        private string attachmentUrl = "https://github.com/apple-touch-icon.png";

        private (TestFlow testFlow, StringLocalizer<PhotoUpdateDialog> localizer) ArrangeTest(string language)
        {
            var accessors = AccessorsFactory.GetAccessors(language);

            // リソースを利用するため StringLocalizer を作成
            var localizer = StringLocalizerFactory.GetStringLocalizer<PhotoUpdateDialog>();

            // Microsoft Graph 系のモック
            var mockGraphSDK = new Mock<IGraphServiceClient>();
            // プロファイル写真の操作をモック
            mockGraphSDK.Setup(x => x.Me.Photo.Content.Request(null).PutAsync(It.IsAny<Stream>()))
                .Returns(Task.FromResult(default(Stream)));

            mockGraphSDK.Setup(x => x.Me.Photo.Content.Request(null).GetAsync())
                .Returns(async () =>
                {
                    return new MemoryStream();
                });

            var msGraphService = new MSGraphService(mockGraphSDK.Object);

            // IServiceProvider のモック
            var serviceProvider = new Mock<IServiceProvider>();

            // PhotoUpdateDialog クラスで解決すべきサービスを登録
            serviceProvider.Setup(x => x.GetService(typeof(LoginDialog))).Returns(new LoginDialog(StringLocalizerFactory.GetStringLocalizer<LoginDialog>()));
            serviceProvider.Setup(x => x.GetService(typeof(MSGraphService))).Returns(new MSGraphService(mockGraphSDK.Object));

            // テスト対象のダイアログをインスタンス化
            var loginDialog = new LoginDialog(StringLocalizerFactory.GetStringLocalizer<LoginDialog>());
            // OAuthPrompt をテスト用のプロンプトに差し替え
            loginDialog.ReplaceDialog(new TestOAuthPrompt("login", new OAuthPromptSettings()));
            var photoUpdateDialog = new PhotoUpdateDialog(serviceProvider.Object, localizer);
            // ログインダイアログを上記でつくったものに差し替え
            photoUpdateDialog.ReplaceDialog(loginDialog);
            var dialogs = new DialogSet(accessors.ConversationDialogState);
            dialogs.Add(photoUpdateDialog);
            dialogs.Add(loginDialog);

            // アダプターを作成し必要なミドルウェアを追加
            var adapter = new TestAdapter()
                .Use(new AutoSaveStateMiddleware(accessors.UserState, accessors.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(PhotoUpdateDialog), attachmentUrl, cancellationToken);
                }
            });

            return (testFlow, localizer);
        }

        [TestMethod]
        [DataRow("ja-JP")]
        [DataRow("en-US")]
        public async Task PhotoUpdateDialogShouldUpdateAndReturnPicture(string language)
        {
            // 言語を指定してテストを作成
            var arrange = ArrangeTest(language);
            Thread.CurrentThread.CurrentCulture = new CultureInfo(language);
            Thread.CurrentThread.CurrentUICulture = new CultureInfo(language);

            await arrange.testFlow
            .Send("foo")
            .AssertReply((activity) =>
            {
                Assert.IsTrue((activity as Activity).Attachments.Count == 1);
            })
            .StartTestAsync();
        }
    }
}
```

2\. ProfileDialogUnitTest.cs を以下のコードと差し替えます。

- 言語を指定したテストのアレンジ
- DataRow を利用したテスト
- アダプティブカードのパスを引数から作成

```csharp:ProfileDialogUnitTest.cs
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Adapters;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Schema;
using Microsoft.Extensions.Localization;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using myfirstbot.unittest.Helpers;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace myfirstbot.unittest
{
    [TestClass]
    public class ProfileDialogUnitTest
    {

        private (TestFlow testFlow, StringLocalizer<ProfileDialog> localizer) ArrangeTest(string language)
        {
            var accessors = AccessorsFactory.GetAccessors(language);

            // リソースを利用するため StringLocalizer を作成
            var localizer = StringLocalizerFactory.GetStringLocalizer<ProfileDialog>();

            // テスト対象のダイアログをインスタンス化
            var dialogs = new DialogSet(accessors.ConversationDialogState);
            dialogs.Add(new ProfileDialog(accessors, localizer));

            // アダプターを作成し必要なミドルウェアを追加
            var adapter = new TestAdapter()
                .Use(new AutoSaveStateMiddleware(accessors.UserState, accessors.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(ProfileDialog), null, cancellationToken);
                }
            });

            return (testFlow, localizer);
        }

        [TestMethod]
        [DataRow("ja-JP")]
        [DataRow("en-US")]
        public async Task ProfileDialog_ShouldSaveProfile(string language)
        {
            // 言語を指定してテストを作成
            var arrange = ArrangeTest(language);
            Thread.CurrentThread.CurrentCulture = new CultureInfo(language);
            Thread.CurrentThread.CurrentUICulture = new CultureInfo(language);

            await arrange.testFlow
            .Send("foo")
            .AssertReply((activity) =>
            {
                // アダプティブカードを比較
                Assert.AreEqual(
                    JObject.Parse((activity as Activity).Attachments[0].Content.ToString()).ToString(),
                    JObject.Parse(File.ReadAllText($"./AdaptiveJsons/{language}/Profile.json")).ToString()
                );
            })
            .Send(new Activity()
            {
                Value = new JObject
                {
                    {"name", "Ken" },
                    {"email" , "kenakamu@microsoft.com"},
                    {"phone" , "xxx-xxxx-xxxx"},
                    {"birthday" , new DateTime(1976, 7, 21)},
                    {"hasCat" , true},
                    {"catNum" , "3"},
                    {"catTypes", "キジトラ,サバトラ,ハチワレ" },
                    {"playWithCat" , true}
                }.ToString()
            })
            .AssertReply((activity) =>
            {
                Assert.AreEqual((activity as Activity).Text, arrange.localizer["save"]);
            })
            .StartTestAsync();
        }
    }
}
```
3\. ScheduleDialogUnitTest.cs を以下のコードと差し替え。

```csharp:ScheduleDialogUnitTest.cs
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Adapters;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Extensions.Localization;
using Microsoft.Graph;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using myfirstbot.unittest.Helpers;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;

namespace myfirstbot.unittest
{
    [TestClass]
    public class ScheduleDialogUnitTest
    {
        // ダミーの予定用の時刻
        DateTime datetime = DateTime.Now;

        private (TestFlow testFlow, StringLocalizer<ScheduleDialog> localizer) ArrangeTest(string language)
        {
            var accessors = AccessorsFactory.GetAccessors(language);

            // リソースを利用するため StringLocalizer を作成
            var localizer = StringLocalizerFactory.GetStringLocalizer<ScheduleDialog>();

            // Microsoft Graph 系のモック
            var mockGraphSDK = new Mock<IGraphServiceClient>();
            // ダミーの予定を返す。
            mockGraphSDK.Setup(x => x.Me.CalendarView.Request(It.IsAny<List<QueryOption>>()).GetAsync())
                .ReturnsAsync(() =>
                {
                    var page = new UserCalendarViewCollectionPage();
                    page.Add(new Event()
                    {
                        Subject = "Dummy 1",
                        Start = new DateTimeTimeZone() { DateTime = datetime.ToString() },
                        End = new DateTimeTimeZone() { DateTime = datetime.AddMinutes(30).ToString() }
                    });
                    page.Add(new Event()
                    {
                        Subject = "Dummy 2",
                        Start = new DateTimeTimeZone() { DateTime = datetime.AddMinutes(60).ToString() },
                        End = new DateTimeTimeZone() { DateTime = datetime.AddMinutes(90).ToString() }
                    });
                    return page;
                });

            // IServiceProvider のモック
            var serviceProvider = new Mock<IServiceProvider>();

            // ScheduleDialog クラスで解決すべきサービスを登録
            serviceProvider.Setup(x => x.GetService(typeof(LoginDialog))).Returns(new LoginDialog(StringLocalizerFactory.GetStringLocalizer<LoginDialog>()));
            serviceProvider.Setup(x => x.GetService(typeof(MSGraphService))).Returns(new MSGraphService(mockGraphSDK.Object));

            // テスト対象のダイアログをインスタンス化
            var loginDialog = new LoginDialog(StringLocalizerFactory.GetStringLocalizer<LoginDialog>());
            // OAuthPrompt をテスト用のプロンプトに差し替え
            loginDialog.ReplaceDialog(new TestOAuthPrompt("login", new OAuthPromptSettings()));
            var scheduleDialog = new ScheduleDialog(serviceProvider.Object, localizer);
            // ログインダイアログを上記でつくったものに差し替え
            scheduleDialog.ReplaceDialog(loginDialog);
            var dialogs = new DialogSet(accessors.ConversationDialogState);
            dialogs.Add(scheduleDialog);
            dialogs.Add(loginDialog);

            // アダプターを作成し必要なミドルウェアを追加
            var adapter = new TestAdapter()
                .Use(new AutoSaveStateMiddleware(accessors.UserState, accessors.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(ScheduleDialog), null, cancellationToken);
                }
            });

            return (testFlow, localizer);
        }

        [TestMethod]
        [DataRow("ja-JP")]
        [DataRow("en-US")]
        public async Task ScheduleDialog_ShouldReturnEvents(string language)
        {
            // 言語を指定してテストを作成
            var arrange = ArrangeTest(language);
            Thread.CurrentThread.CurrentCulture = new CultureInfo(language);
            Thread.CurrentThread.CurrentUICulture = new CultureInfo(language);

            await arrange.testFlow
            .Send("foo")
            .AssertReply($"{datetime.ToString("HH:mm")}-{datetime.AddMinutes(30).ToString("HH:mm")} : Dummy 1")
            .AssertReply($"{datetime.AddMinutes(60).ToString("HH:mm")}-{datetime.AddMinutes(90).ToString("HH:mm")} : Dummy 2")
            .StartTestAsync();
        }
    }
}
```

## MyBotUnitTest の更新

MyBot は実装を忘れたのか、チェックインを忘れたのか分かりませんが、リソースファイルでの多言語化は行っていません。またプロファイルで UserProfile を使うパターンと使わないパターンがあります。よって以下の様に変更します。

1\. MyBotUnitTest.cs の ArrangeTest メソッドを以下のコードと差し替え。

- Accessors ファクトリーに ReturnUserProfile プロパティを渡す
- 言語については ja-JP を利用

```csharp:MyBotUnitTest.cs
private (TestFlow testFlow, BotAdapter adapter, DialogSet dialogs) ArrangeTest(bool returnUserProfile)
{
    // アダプターを作成
    var adapter = new TestAdapter();

    var accessors = AccessorsFactory.GetAccessors("ja-JP", returnUserProfile);

    // IServiceProvider のモック
    var serviceProvider = new Mock<IServiceProvider>();

    // MyBot クラスで解決すべきサービスを登録
    serviceProvider.Setup(x => x.GetService(typeof(LoginDialog))).Returns(new LoginDialog(StringLocalizerFactory.GetStringLocalizer<LoginDialog>()));
    serviceProvider.Setup(x => x.GetService(typeof(WeatherDialog))).Returns(new WeatherDialog(accessors, StringLocalizerFactory.GetStringLocalizer<WeatherDialog>()));
    serviceProvider.Setup(x => x.GetService(typeof(ProfileDialog))).Returns(new ProfileDialog(accessors, StringLocalizerFactory.GetStringLocalizer<ProfileDialog>()));
    serviceProvider.Setup(x => x.GetService(typeof(SelectLanguageDialog))).Returns(new SelectLanguageDialog(accessors));
    serviceProvider.Setup(x => x.GetService(typeof(WelcomeDialog))).Returns
        (new WelcomeDialog(accessors, null, serviceProvider.Object));
    serviceProvider.Setup(x => x.GetService(typeof(ScheduleDialog))).Returns(new ScheduleDialog(serviceProvider.Object, StringLocalizerFactory.GetStringLocalizer<ScheduleDialog>()));
    serviceProvider.Setup(x => x.GetService(typeof(MenuDialog))).Returns(new MenuDialog(serviceProvider.Object, StringLocalizerFactory.GetStringLocalizer<MenuDialog>()));
    serviceProvider.Setup(x => x.GetService(typeof(PhotoUpdateDialog))).Returns(new PhotoUpdateDialog(serviceProvider.Object, StringLocalizerFactory.GetStringLocalizer<PhotoUpdateDialog>()));


    // IRecognizer のモック化
    var mockRecognizer = new Mock<IRecognizer>();
    mockRecognizer.Setup(l => l.RecognizeAsync(It.IsAny<TurnContext>(), It.IsAny<CancellationToken>()))
        .Returns((TurnContext turnContext, CancellationToken cancellationToken) =>
        {
            // RecognizerResult の作成
            var recognizerResult = new RecognizerResult()
            {
                Intents = new Dictionary<string, IntentScore>(),
                Entities = new JObject()
            };

            switch (turnContext.Activity.Text)
            {
                case "キャンセル":
                    recognizerResult.Intents.Add("Cancel", new IntentScore() { Score = 1 });
                    break;
                case "天気を確認":
                    recognizerResult.Intents.Add("Weather", new IntentScore() { Score = 1 });
                    break;
                case "今日の天気を確認":
                    recognizerResult.Intents.Add("Weather", new IntentScore() { Score = 1 });
                    recognizerResult.Entities.Add("day", JArray.Parse("[['今日']]"));
                    break;
                case "ヘルプ":
                    recognizerResult.Intents.Add("Help", new IntentScore() { Score = 1 });
                    break;
                case "プロファイルの変更":
                    recognizerResult.Intents.Add("Profile", new IntentScore() { Score = 1 });
                    break;
                default:
                    recognizerResult.Intents.Add("None", new IntentScore() { Score = 1 });
                    break;
            }
            return Task.FromResult(recognizerResult);
        });
    // テスト対象のクラスをインスタンス化
    var bot = new MyBot(accessors, mockRecognizer.Object, serviceProvider.Object);

    // 差し替える必要があるものを差し替え
    var photoUpdateDialog = new DummyDialog(nameof(PhotoUpdateDialog));
    bot.ReplaceDialog(photoUpdateDialog);

    // DialogSet を作成したクラスより Refactor
    var dialogSet = (DialogSet)typeof(MyBot).GetField("dialogs", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(bot);
    // TestFlow の作成
    var testFlow = new TestFlow(adapter, bot.OnTurnAsync);
    return (testFlow, adapter, dialogSet);
}
```

2\. MyBot_ShouldGoToWeatherDialogWithEntityResult メソッドを削除。

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

最後にすべてコンパイルして、テストを実行します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/8f4b84b3-b06a-9740-587f-5c52b789a0a8.png)

# まとめ

アダプティブカードのテストはリソースファイルから取得できない文字列に対するテストを工夫する必要がありましたが、今回は DataRow の複数パラメーターを使って対応できました。

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

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