0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Bot Builder v4 でのテスト : リソースファイルを使って多言語対応したボットのユニットテスト

Last updated at Posted at 2019-03-03

今回はリソースファイルを使って多言語対応したボットのユニットテストを見ていきます。ボットの多言語化については Bot Builder v4 でボット開発 : 多言語対応を行う - リソースファイルの利用 を参照してください。

ソリューションの準備

ボットのコードは Bot Builder v4 でボット開発 : 多言語対応を行う - リソースファイルの利用 で開発したものを使うので、コードの詳細はそちらの記事を参照してください。また前回の記事で開発した test-article12 のコードをベースに、article15 ブランチのコードをマージしてテストを開発します。

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

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

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

git checkout article15
git checkout test-article12

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

git checkout -b test-article13

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

git merge article15

image.png

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

git mergetool

image.png

6. マージのツールを起動しようとするので既定のまま Enter キーを押下。
image.png

7. 全てのソースにおいて、競合箇所はソースを適用してマージを実行。マージ完了後、Dialogs、Middlewares、Resources フォルダを myfirstbot フォルダへ移動。
image.png

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

PhotoUpdateDialog.csの場合
public class PhotoUpdateDialog : ComponentDialog
{
    private IServiceProvider serviceProvider;
    private MSGraphService graphClient;

    public PhotoUpdateDialog(IServiceProvider serviceProvider) : base(nameof(PhotoUpdateDialog))
    {
        this.serviceProvider = serviceProvider;
        graphClient = (MSGraphService)serviceProvider.GetService(typeof(MSGraphService));
...
ScheduleDialog.csの場合
public class ScheduleDialog : ComponentDialog
{
    private IServiceProvider serviceProvider;
    private MSGraphService graphClient;

    public ScheduleDialog(IServiceProvider serviceProvider) : base(nameof(ScheduleDialog))
    {
        this.serviceProvider = serviceProvider;
        graphClient = (MSGraphService)serviceProvider.GetService(typeof(MSGraphService));
...

9. myfirstbot プロジェクトだけビルドしてエラーが出ないことを確認。

ダイアログの IoC 対応

Bot Builder v4 でボット開発 : 多言語対応を行う - リソースファイルの利用 では、各種ダイアログを IoC 化し、様々なクラスのコンストラクタに IServiceProvider を渡してダイアログを解決しています。ユニットテストにおいても IServiceProvider に相当するものをモック化する必要があります。今回は以下のようなコードで必要なサービスを返すようにしてみました。

var serviceProvider = new Mock<IServiceProvider>();

serviceProvider.Setup(x => x.GetService(typeof(LoginDialog))).Returns(new LoginDialog());
serviceProvider.Setup(x => x.GetService(typeof(WeatherDialog))).Returns(new WeatherDialog());
serviceProvider.Setup(x => x.GetService(typeof(ProfileDialog))).Returns(new ProfileDialog(accessors));
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));
serviceProvider.Setup(x => x.GetService(typeof(MenuDialog))).Returns(new MenuDialog(serviceProvider.Object));
serviceProvider.Setup(x => x.GetService(typeof(PhotoUpdateDialog))).Returns(new PhotoUpdateDialog(serviceProvider.Object));

MyBotUnitTest.cs の改修

上記の手法を使ってテストを改修します。

1. MyBotUnitTest.cs の ArrangeTest メソッドを以下の様に変更。

  • IServiceProvider のモック化
  • DialogSet をリフレクションで取得
private (TestFlow testFlow, BotAdapter adapter, DialogSet dialogs) ArrangeTest(bool returnUserProfile)
{
    // アダプターを作成
    var adapter = new TestAdapter();
 
    // ストレージとしてモックのストレージを利用
    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 = name, Age = 0 } }
        });
    }
    // ストレージへの読み書きを設定
    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")
    };

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

    // MyBot クラスで解決すべきサービスを登録
    serviceProvider.Setup(x => x.GetService(typeof(LoginDialog))).Returns(new LoginDialog());
    serviceProvider.Setup(x => x.GetService(typeof(WeatherDialog))).Returns(new WeatherDialog());
    serviceProvider.Setup(x => x.GetService(typeof(ProfileDialog))).Returns(new ProfileDialog(accessors));
    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));
    serviceProvider.Setup(x => x.GetService(typeof(MenuDialog))).Returns(new MenuDialog(serviceProvider.Object));
    serviceProvider.Setup(x => x.GetService(typeof(PhotoUpdateDialog))).Returns(new PhotoUpdateDialog(serviceProvider.Object));

    // 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_ShouldGoToProfileDialogWithConversationUpdateWithoutUserProfile メソッドを削除。代わりに MyBot_ShouldGoToSelectLanguageDialogWithConversationUpdateWithoutUserProfile メソッドを追加。

[TestMethod]
public async Task MyBot_ShouldGoToSelectLanguageDialogWithConversationUpdateWithoutUserProfile()
{
    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;
            // 現在のダイアログスタックの一番上が SelectLanguageDialog であることを確認。
            var dialogInstances = (dc.Stack.Where(x => x.Id == nameof(WelcomeDialog)).First().State["dialogs"] as DialogState).DialogStack;
            Assert.AreEqual(dialogInstances[0].Id, "SelectLanguageDialog");
        })
        .StartTestAsync();
}

MenuDialogUnitTest の改修

MyBotUnitTest.cs と同様に、IServiceProvider のモックを入れます。

1. 必要な using を追加。

using System;
using Moq;

2. ArrangeTest メソッドを以下のコードに変更。

  • IServiceProvider のモック化
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")
    };

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

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

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

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

    return (testFlow, adapter, dialogs);
}

ScheduleDialogUnitTest の改修

こちらも同様に IServiceProvider のモックを使います。

1. ScheduleDialogUnitTest.cs の ArrangeTestFlow を以下のコードと差し替え。

private TestFlow ArrangeTestFlow()
{
    // ストレージとしてインメモリを利用
    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")
    };

    // 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());
    serviceProvider.Setup(x => x.GetService(typeof(MSGraphService))).Returns(new MSGraphService(mockGraphSDK.Object));

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

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

    // TestFlow の作成
    return 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);
        }
    });
}

PhotoUpdateDialog の改修

こちらも同様に IServiceProvider のモックを使います。

1. PhotoUpdateDialog.cs の ArrangeTestFlow を以下のコードと差し替え。

private TestFlow ArrangeTestFlow()
{
    // ストレージとしてインメモリを利用
    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")
    };

    // 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());
    serviceProvider.Setup(x => x.GetService(typeof(MSGraphService))).Returns(new MSGraphService(mockGraphSDK.Object));
            
    // テスト対象のダイアログをインスタンス化
    var loginDialog = new LoginDialog();
    // OAuthPrompt をテスト用のプロンプトに差し替え
    loginDialog.ReplaceDialog(new TestOAuthPrompt("login", new OAuthPromptSettings()));
    var photoUpdateDialog = new PhotoUpdateDialog(serviceProvider.Object);
    // ログインダイアログを上記でつくったものに差し替え
    photoUpdateDialog.ReplaceDialog(loginDialog);
    var dialogs = new DialogSet(accessors.ConversationDialogState);
    dialogs.Add(photoUpdateDialog);
    dialogs.Add(loginDialog);

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

    // TestFlow の作成
    return 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);
        }
    });
}

リソースファイルを使った多言語のユニットテスト

最後は WelcomeDialogUnitTest ですが、このダイアログはリソースを使っているため、テストもそれに対応する必要があります。リソースファイルの利用は外部サービスの利用でないためモックする必要はなく、必要なクラスのインスタンスを作成して利用します。

1. myfirstbot.unittest プロジェクトを右クリックして「myfirstbot.unittst.csproj の編集」をクリック。
image.png

2. myfirstbot\Resources のファイルをユニットテストでも利用できるよう、ItemGroup を追加。

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

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

  • 引数でテストする言語を指定
  • IServiceProvider のモック化
  • StringLocalizer を Resource フォルダにある resx ファイルを利用して作成
  • 戻り値のタプルに StringLocalizer も追加
private (TestFlow testFlow, BotAdapter adapter, DialogSet dialogs, StringLocalizer<WelcomeDialog> localizer) ArrangeTest(string language)
{
    // ストレージとしてインメモリを利用
    IStorage dataStore = new MemoryStorage();
    // それぞれのステートを作成
    var mockStorage = new Mock<IStorage>();
    // User1用に返すデータを作成
    // UserState のキーは <channelId>/users/<userId>
    var dictionary = new Dictionary<string, object>();
    // ユーザープロファイルを設定。
    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")
    };

    // リソースを利用するため StringLocalizer を作成
    ResourceManagerStringLocalizerFactory factory = new ResourceManagerStringLocalizerFactory(
        Options.Create(new LocalizationOptions() { ResourcesPath = "Resources" }), NullLoggerFactory.Instance);
    var localizer = new StringLocalizer<WelcomeDialog>(factory);

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

    // WelcomeDialog クラスで解決すべきサービスを登録
    serviceProvider.Setup(x => x.GetService(typeof(ProfileDialog))).Returns(new ProfileDialog(accessors));
    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(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, localizer);
}

4. WelcomeDialog_ShouldGoToProfileDialog メソッドを以下の様に変更。

  • 複数言語を DataRow で指定してテスト
  • テスト自体の Culture も指定
  • Assert で localizer を使用
[TestMethod]
[DataRow("ja-JP")]
[DataRow("en-US")]
public async Task WelcomeDialog_ShouldGoToProfileDialog(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) =>
    {
        // Activity にヒーローカードが含まれていることを確認。
        Assert.AreEqual((activity as Activity).Attachments.Count, 1);
        var heroCard = (activity as Activity).Attachments.First().Content as HeroCard;
        // ヒーローカードの内容を確認。
        Assert.AreEqual(heroCard.Title, arrange.localizer["title"]);
        Assert.AreEqual(heroCard.Buttons.Where(x => x.Title == arrange.localizer["yes"]).First().Value, arrange.localizer["yes"].ToString());
        Assert.AreEqual(heroCard.Buttons.Where(x => x.Title == arrange.localizer["skip"]).First().Value, arrange.localizer["skip"].ToString());
        Assert.AreEqual(heroCard.Buttons.Where(x => x.Title == arrange.localizer["checkDetail"]).First().Value, "https://dev.botframework.com");
    })
    .Send(arrange.localizer["yes"])
    .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();
}

5. 同様に WelcomeDialog_ShouldSetAnonymous メソッドも更新。

[TestMethod]
[DataRow("ja-JP")]
[DataRow("en-US")]
public async Task WelcomeDialog_ShouldSetAnonymous(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) =>
    {
        // Activity にヒーローカードが含まれていることを確認。
        Assert.AreEqual((activity as Activity).Attachments.Count, 1);
        var heroCard = (activity as Activity).Attachments.First().Content as HeroCard;
        // ヒーローカードの内容を確認。
        Assert.AreEqual(heroCard.Title, arrange.localizer["title"]);
        Assert.AreEqual(heroCard.Buttons.Where(x => x.Title == arrange.localizer["yes"]).First().Value, arrange.localizer["yes"].ToString());
        Assert.AreEqual(heroCard.Buttons.Where(x => x.Title == arrange.localizer["skip"]).First().Value, arrange.localizer["skip"].ToString());
        Assert.AreEqual(heroCard.Buttons.Where(x => x.Title == arrange.localizer["checkDetail"]).First().Value, "https://dev.botframework.com");
    })
    .Send(arrange.localizer["skip"])
    .AssertReply((activity) =>
    {
        // 返ってきたテキストが匿名かを確認
        Assert.AreEqual((activity as Activity).Text, arrange.localizer["anonymous"].ToString());
    })
    .StartTestAsync();
}

SetLanguageDialog のユニットテスト

最後に新規に追加された SetLanguageDialog のユニットテストを追加します。

1. SetLanguageDialogUnitTest.cs を追加し、以下のコードに書き換え。

using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Adapters;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace myfirstbot.unittest
{
    [TestClass]
    public class SelectLanguageDialogUnitTest
    {
        private TestFlow ArrangeTest()
        {
            // ストレージとしてインメモリを利用
            IStorage dataStore = new MemoryStorage();
            // それぞれのステートを作成
            var mockStorage = new Mock<IStorage>();
            // User1用に返すデータを作成
            // UserState のキーは <channelId>/users/<userId>
            var dictionary = new Dictionary<string, object>();
            // ユーザープロファイルを設定。
            dictionary.Add("test/users/user1", new Dictionary<string, object>()
                {
                    { "UserProfile", new UserProfile() { Name = "Ken", Age = 0} }
                });
            // ストレージへの読み書きを設定
            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")
            };

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

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

            // TestFlow の作成
            return 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(SelectLanguageDialog), null, cancellationToken);
                }
                // ダイアログが完了した場合は、UserProfile の言語をテスト側に返す
                else if (results.Status == DialogTurnStatus.Complete)
                {
                    await turnContext.SendActivityAsync((await accessors.UserProfile.GetAsync(turnContext)).Language);
                }
            });
        } 

        [TestMethod]
        public async Task SelectLanguageDialog_ShouldSetJapanese()
        {
            // テストの追加と実行
            await ArrangeTest()
            .Test("foo", "言語を選択してください。Select your language (1) 日本語 or (2) English")
            .Test("日本語","ja-JP")
            .StartTestAsync();
        }

        [TestMethod]
        public async Task SelectLanguageDialog_ShouldSetEnglish()
        {
            // テストの追加と実行
            await ArrangeTest()
            .Test("foo", "言語を選択してください。Select your language (1) 日本語 or (2) English")
            .Test("English", "en-US")
            .StartTestAsync();
        }
    }
}

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

コンパイルして全てのテスト結果を確認します。
image.png

まとめ

今回はリソースファイルを使った多言語対応のユニットテストを行いました。ダイアログの IoC 化も入っていたので少し対応量が多くなりましたが、リソースファイルを再利用したテストも無事できました。

次の記事へ
目次に戻る

この記事のサンプルコード

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?