今回は単一の高度なダイアログのユニットテストを試してみます。高度なダイアログについては
Bot Builder v4 でボット開発 : より高度なダイアログ を参照してください。
ソリューションの準備
ボットのコードは Bot Builder v4 でボット開発 : より高度なダイアログ で開発したものを使うので、コードの詳細はそちらの記事を参照してください。また前回の記事で開発した test-article3 のコードをベースに、article5 ブランチのコードをマージしてテストを開発します。
1. 任意のフォルダでレポジトリをクローン。
git clone https://github.com/kenakamu/botbuilderv4completeguide/
cd botbuilderv4completeguide
2. 以下のコマンドで article5 をチェックアウトした後、test-article3 をチェックアウトしてどちらもローカルにコピー。
git checkout article5
git checkout test-article3
3. 以下コマンドで test-article4 ブランチを作成。
git checkout -b test-article4
4. article5 のブランチをマージ。
git merge article5
5. 以下の結果より既存ファイルは myfirstbot フォルダにマージされ、 Models/UserProfile.cs と ProfileDialog.cs はルートにコピーされたことを確認。
6. ルートに作成されたものを myfirstbot に移動。
move Models myfirstbot
move ProfileDialog.cs myfirstbot
7. myfirstbot.sln を Visual Studio で開き、ソリューションをビルド。ビルドに失敗することを確認。
今回のボットはステート管理で UserState も必要のため、以下でこの点も直しながらテストを実装していきます。
ロケールの設定
既定で提供されるプロンプトは、ComfirmPrompt のように Activity のロケールによって応答が変わるものがあります。TestFlow を使ったテストでロケールを指定したい場合、いくつか方法があります。
- Activity を作って、TestFlow のメソッドに送る
- ミドルウェアを使って TurnContext.Activity のロケールを差し替える
- TestFlow と TestAdapter に拡張メソッドを追加してロケールに対応する
極力簡単にテストをしたいので、今回はミドルウェアを使った方法を利用してみます。
テストの実装と実行
高度なダイアログのテストをする場合、以下の 2 通りの方法が考えられます。
- IBot のメインからダイアログを呼ぶところも含めてテスト
- ダイアログ単体でテスト
それぞれのやり方を見ていきます。
IBot からテスト
まずは MyBot クラスから ProfileDialog を呼ぶ流れも含めてテストしてみます。
1. MyBotUnitTest.cs ファイルの MyBot_ShouldAskName メソッドを以下のコードに差し替え。必要に応じて using を追加。
- アクセサーに UserState を渡し、UserProfile プロパティを作成。
[TestMethod]
public async Task MyBot_ShouldSaveProfile()
{
// アダプターを作成
var adapter = new TestAdapter();
// ストレージとしてインメモリを利用
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 bot = new MyBot(accessors);
// テスト用の変数
var name = "Ken";
var age = "42";
// テストの追加と実行
await new TestFlow(adapter, bot.OnTurnAsync)
.Test("foo", "名前を入力してください。")
.Test(name, "年齢を聞いてもいいですか?")
.Test("はい", "年齢を入力してください。")
.Test(age, $"次の情報で登録します。いいですか?{Environment.NewLine} 名前:{name} 年齢:{age}")
.Test("はい", "プロファイルを保存します。")
.StartTestAsync();
}
2. プロジェクトをビルドして、該当のテストを実行。以下の様に失敗することを確認。ここには 2 つ問題がる。
3. myfirstbot.unittest プロジェクトに SetLocaleMiddleware.cs を追加して以下コードを張り付け。
using Microsoft.Bot.Builder;
using Microsoft.Recognizers.Text;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace myfirstbot.unittest
{
public class SetLocaleMiddleware : IMiddleware
{
private string locale = string.Empty;
public SetLocaleMiddleware(string locale)
{
// 指定したロケールがサポートされたものであれば設定保持。違う場合は en-us を設定。
if (Culture.SupportedCultures.Where(x => x.CultureName == locale) != null)
{
this.locale = locale;
}
else
{
this.locale = Culture.English;
}
}
public async Task OnTurnAsync(ITurnContext turnContext, NextDelegate next, CancellationToken cancellationToken = default(CancellationToken))
{
turnContext.Activity.Locale = locale;
await next.Invoke(cancellationToken);
}
}
}
4. MyBot_ShouldSaveProfile テストを以下の様に変更。必要に応じて using を追加。
- アダプターにミドルウェアを追加
[TestMethod]
public async Task MyBot_ShouldSaveProfile()
{
// アダプターを作成
var adapter = new TestAdapter();
adapter.Use(new SetLocaleMiddleware(Culture.Japanese));
// ストレージとしてインメモリを利用
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 bot = new MyBot(accessors);
// テスト用の変数
var name = "Ken";
var age = "42";
// テストの追加と実行
await new TestFlow(adapter, bot.OnTurnAsync)
.Test("foo", "名前を入力してください。")
.Test(name, "年齢を聞いてもいいですか? (1) はい または (2) いいえ")
.Test("はい", "年齢を入力してください。")
.Test(age, $"次の情報で登録します。いいですか?{Environment.NewLine} 名前:{name} 年齢:{age} (1) はい または (2) いいえ")
.Test("はい", "プロファイルを保存します。")
.StartTestAsync();
}
6. 他のパターンのテストも追加。
[TestMethod]
public async Task MyBot_ShouldNotSaveProfile()
{
// アダプターを作成
var adapter = new TestAdapter();
adapter.Use(new SetLocaleMiddleware(Culture.Japanese));
// ストレージとしてインメモリを利用
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 bot = new MyBot(accessors);
// テスト用の変数
var name = "Ken";
var age = "42";
// テストの追加と実行
await new TestFlow(adapter, bot.OnTurnAsync)
.Test("foo", "名前を入力してください。")
.Test(name, "年齢を聞いてもいいですか? (1) はい または (2) いいえ")
.Test("はい", "年齢を入力してください。")
.Test(age, $"次の情報で登録します。いいですか?{Environment.NewLine} 名前:{name} 年齢:{age} (1) はい または (2) いいえ")
.Test("いいえ", "プロファイルを破棄します。")
.StartTestAsync();
}
[TestMethod]
public async Task MyBot_ShouldSaveProfileWithoutAge()
{
// アダプターを作成
var adapter = new TestAdapter();
adapter.Use(new SetLocaleMiddleware(Culture.Japanese));
// ストレージとしてインメモリを利用
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 bot = new MyBot(accessors);
// テスト用の変数
var name = "Ken";
var age = 0;
// テストの追加と実行
await new TestFlow(adapter, bot.OnTurnAsync)
.Test("foo", "名前を入力してください。")
.Test(name, "年齢を聞いてもいいですか? (1) はい または (2) いいえ")
.Test("いいえ", $"次の情報で登録します。いいですか?{Environment.NewLine} 名前:{name} 年齢:{age} (1) はい または (2) いいえ")
.Test("はい", "プロファイルを保存します。")
.StartTestAsync();
}
ダイアログ単体でテスト
次にダイアログ単体でテストする方法を見ていきます。ダイアログ単体でテストする場合は、MyBot.cs で作っているようなダイアログ呼び出しロジックをテスト側で作るとともに、ステートの保存も AutoSaveStateMiddleware を使って実行します。
1. MyBotUnitTest.cs に以下のメソッドを追加。
- TestAdapter で AutoSaveStateMiddleware を指定
- AutoSaveStateMiddleware で保存したい BotState を引き渡し
- TestFlow のコールバックにダイアログを起動するだけのコードを追加
[TestMethod]
public async Task ProfileDialog_ShouldSaveProfile()
{
// ストレージとしてインメモリを利用
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 ProfileDialog(accessors));
// アダプターを作成し必要なミドルウェアを追加
var adapter = new TestAdapter()
.Use(new SetLocaleMiddleware(Culture.Japanese))
.Use(new AutoSaveStateMiddleware(userState, conversationState));
// テスト用の変数
var name = "Ken";
var age = "42";
// テストの追加と実行
await 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);
}
})
.Test("foo", "名前を入力してください。")
.Test(name, "年齢を聞いてもいいですか? (1) はい または (2) いいえ")
.Test("はい", "年齢を入力してください。")
.Test(age, $"次の情報で登録します。いいですか?{Environment.NewLine} 名前:{name} 年齢:{age} (1) はい または (2) いいえ")
.Test("はい", "プロファイルを保存します。")
.StartTestAsync();
}
3. 同様に残りのパターンも追加。
[TestMethod]
public async Task ProfileDialog_ShouldNotSaveProfile()
{
// ストレージとしてインメモリを利用
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 ProfileDialog(accessors));
// アダプターを作成し必要なミドルウェアを追加
var adapter = new TestAdapter()
.Use(new SetLocaleMiddleware(Culture.Japanese))
.Use(new AutoSaveStateMiddleware(userState, conversationState));
// テスト用の変数
var name = "Ken";
var age = "42";
// テストの追加と実行
await 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);
}
})
.Test("foo", "名前を入力してください。")
.Test(name, "年齢を聞いてもいいですか? (1) はい または (2) いいえ")
.Test("はい", "年齢を入力してください。")
.Test(age, $"次の情報で登録します。いいですか?{Environment.NewLine} 名前:{name} 年齢:{age} (1) はい または (2) いいえ")
.Test("いいえ", "プロファイルを破棄します。")
.StartTestAsync();
}
[TestMethod]
public async Task ProfileDialog_ShouldSaveProfileWithoutAge()
{
// ストレージとしてインメモリを利用
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 ProfileDialog(accessors));
// アダプターを作成し必要なミドルウェアを追加
var adapter = new TestAdapter()
.Use(new SetLocaleMiddleware(Culture.Japanese))
.Use(new AutoSaveStateMiddleware(userState, conversationState));
// テスト用の変数
var name = "Ken";
var age = 0;
// テストの追加と実行
await 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);
}
})
.Test("foo", "名前を入力してください。")
.Test(name, "年齢を聞いてもいいですか? (1) はい または (2) いいえ")
.Test("いいえ", $"次の情報で登録します。いいですか?{Environment.NewLine} 名前:{name} 年齢:{age} (1) はい または (2) いいえ")
.Test("はい", "プロファイルを保存します。")
.StartTestAsync();
}
まとめ
今回はロケールの設定方法やダイアログのテスト方法を数種類紹介しましたが、基本はダイアログ単位でテストが行えるよう実装してください。次回はさらに複数のダイアログがある場合のテストを見ていきます。