Bot Builder v4 でのテスト : SDK 4.3 での OAuthPrompt ユニットテスト


今回は SDK 4.3 で追加された OAuthPrompt のユニットテストを見ていきます。

以前の OAuthPrompt はユニットテストできるように設計されていなかったため、このシリーズでもかなり無理やりモック化しましたが、PR 1357 によって対応されました。

ソリューションの準備

前回の記事で開発した test-article17 のコードをベースに開発します。

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

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

2. 以下のコマンドで test-article17 をチェックアウト。

git checkout test-article17

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

git checkout -b test-article18

4. ユニットテストプロジェクトから Helpers/TestOAuthPrompt.cs を削除。

5. ソリューションをビルドして、TestOAuthPrompt の参照エラーが出ることを確認。

OAuthPrompt のモック

PR 1357 によって OAuthPrompt が BotFrameworkAdapter に依存することがなくなり、TestAdapter で動作するようになりました。変更内容は Files Changes を参照してください。

基本的には TestAdapter の AddUserToken メソッドによりダミーのトークンを設定すれば自動的にログインが完了した状態となります。

ユニットテストの更新

1. ダミーのログインダイアログを作成していた個所をコメントアウト。

  • ログインダイアログの新規作成とリプレース部分をコメントアウト
// テスト対象のダイアログをインスタンス化
// var loginDialog = new LoginDialog(StringLocalizerFactory.GetStringLocalizer<LoginDialog>());
//// OAuthPrompt をテスト用のプロンプトに差し替え
// loginDialog.ReplaceDialog(new TestOAuthPrompt("login", new OAuthPromptSettings()));

var scheduleNotificationStore = new ScheduleNotificationStore();
var scheduleDialog = new ScheduleDialog(accessors, serviceProvider.Object, localizer, scheduleNotificationStore);
// ログインダイアログを上記でつくったものに差し替え
// scheduleDialog.ReplaceDialog(loginDialog);
var dialogs = new DialogSet(accessors.ConversationDialogState);
dialogs.Add(scheduleDialog);
//dialogs.Add(loginDialog);

2. TestAdapter 作成の後に、ダミートークンを設定するコードを追加。

  • ConnectionName は ScheduleDialog と同じものを指定
  • ユーザー名やチャネル名などは GitHub 上の TestAdapter.cs より取得
// アダプターからダミーログインを返すように設定
adapter.AddUserToken("AzureAdv2", "test", "user1", "dummyToken");

3. 同様に PhotoUpdateDialogUnitTest.cs の ArrangeTest メソッドも更新。

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

    // アダプターからダミーログインを返すように設定
    adapter.AddUserToken("AzureAdv2", "test", "user1", "dummyToken");

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

4. ソリューションをビルドしてエラーがないことを確認後、全てのテストを実行。

5. TestOAuthPrompt の差し替え用に用意した DialogComponentExtensions.cs も不要となったため、削除。

まとめ

前の苦労が嘘のように簡単に実行できるようになりました。依存サービスはモック化できるようにしておくべきといういい例になったと考えています。尚、PR はユニットテストもついていることが多いため、そちらを見ることでどのように機能が使えるかも分かります。

詳細は TestAdapterTests.cs を確認してください。

次の記事へ
目次に戻る

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