Bot Builder v4 でのテスト : ユニットテストの概要と準備

このシリーズでは BotBuilder v4 におけるテストを見ていきます。開発自体は、「[Bot Builder v4 でボット開発](https://qiita.com/kenakamu/items/6dc043cfc1f199032883#bot-builder-v4-%E3%81%A7%E3%83%9C%E3%83%83%E3%83%88%E9%96%8B%E7%99%BA)」を参照してください。

今回はユニットテストの概要と準備を行います。

# ユニットテストの概要

ユニットテストは最小限単位のコードが意図通りに動作するかをテストするもので、通常はメソッド毎にテストを作ります。BotBuilder におけるテストのポイントを以下に挙げます。

## テストの単位

オブジェクト指向開発で実装したクラスやメソッドと異なり、会話サービスとして開発されるボットは複数のやり取りが最小限の単位となる場合が多くあります。また継続した会話を成立するためにはステートを準備する必要がありますが、テストプロジェクトの開発工数を下げるため一連の流れをユニットテストの最小限単位としたほうが良い場合もあります。

私が普段テストの対象と単位は以下の通りです。

- IBot からダイアログを呼び出すまで
- 各ダイアログ全体または適切な単位
- ミドルウェア
- サービスラッパー
- プロンプト検証メソッド
- ASP.NET コントローラー

## テストの境界線

ボットサービスはサーバーサイドとクライアントサイドに分かれますが、クライアント側のテストは行わないため、アダプター以降のコードをのみをテスト対象とすることが多いです。

## 依存サービス

外部 API やデータベース呼び出しなどの依存サービスはテストに影響します。

- テストに必要なデータを返してほしい
- 外部サービスにエラーがあるとテストの結果にも影響がある

上記の理由から、ユニットテストでは依存サービスはモックを作成して利用します。この辺りは通常のユニットテストと同様です。よく使われる依存サービスは以下の通りです。

- LUIS
- QnA
- 翻訳サービス
- その他、Microsoft Graph や天気 API のような外部 API

## ステート管理

BotBuilder の重要な機能としてステート管理があります。本番で使う仕組みに関わらず、テスト時には MemoryStorage を使います。

## テストフレームワーク

好きなものを使ってください。このシリーズでは MSTest と Moq を使います。必要に応じて Fake を使うかもしれませんが現時点ではまだ分かりません。

# ユニットテストで使える BotBuilder SDK ライブラリ

BotBuilder に含まれる Adapters 空間にはユニットテストで利用可能なクラスが提供されます。

## TestAdapter クラス

[GitHub: TestAdapter.cs](https://github.com/Microsoft/botbuilder-dotnet/blob/master/libraries/Microsoft.Bot.Builder/Adapters/TestAdapter.cs)

テスト用のアダプターを提供します。ユーザーからのインプットとボットからのアウトプットを扱えます。

## TestFlow クラス

[GitHub: TestFlow.cs](https://github.com/Microsoft/botbuilder-dotnet/blob/master/libraries/Microsoft.Bot.Builder/Adapters/TestFlow.cs)
アダプターとテストするロジックを指定することにより、ボットのテストを行うことが出来ます。以下に主なメソッドを紹介します。

### Send メソッド

ユーザーインプットを送ります。

### AssetReply メソッド

ボットからのアウトプットを検証します。

### Test メソッド

Send と AssetReply を内部的に実装したラッパーメソッドです。

### StartTestAsync メソッド

設定したテストを実行します。

# 準備

ここではまずシンプルなボットのテストを実装してみます。

1\. Visual Studio から新しいプロジェクトを作成。EchoBot を選択して作成。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/ad6614c5-9d94-a448-9d29-82b3c1ed5615.png)

2\. ソリューションを右クリックして、新しいプロジェクトの追加を選択。.NET Core より MSTest テストプロジェクトを選択して作成。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/c0bb9508-75dc-37fc-c096-5206dc99e8cd.png)

3\. ソリューションを右クリックして「ソリューションの NuGet パッケージの管理」を選択。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/5e06488c-e4b3-b6d6-984a-256fda5767be.png)

4\. 以下パッケージをテストプロジェクトにも追加。

Microsoft.AspNetCore
Microsoft.Bot.Builder.Integration.AspNet.Core

5\. テストプロジェクトの依存関係に EchoBot1 を追加。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/306b5b06-9004-9bac-677f-53b1b98816ae.png)

6\. ボットがスタートアッププロジェクトになっていることを確認してから、F5 を押下してデバッグ実行。エミュレーターよりボットが動作することを確認。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/528586ee-ccad-5176-99f4-c358391f9bac.png)

# テストの実装

準備が完了したのでテストを実装します。

1\. テストプロジェクトにある UnitTest1.cs ファイルを EchoBotUnitTest.cs に変更。FirstEchoTest メソッドを追加。

- テストは非同期処理のため、シグネチャーは async Task

```csharp
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Threading.Tasks;

namespace EchoBot1.UnitTest
{
    [TestClass]
    public class EchoBotUnitTest
    {
        [TestMethod]
        public async Task FirstEchoTest()
        {
        }
    }
}
```
2\. まず TestAdapter をインスタンス化。必要に応じて using を追加。

```csharp
// アダプターを作成
var adapter = new TestAdapter();
```

3\. 今回テストする EchoBot1Bot クラスはコンストラクタとしてアクセサーと ILoggerFactory を渡す必要があるため、それぞれを追加。必要に応じて using を追加。

```csharp
// ステート管理のためアクセサーを作成
IStorage dataStore = new MemoryStorage();
var conversationState = new ConversationState(dataStore);
var accessors = new EchoBot1Accessors(conversationState)
{
    CounterState = conversationState.CreateProperty<CounterState>(EchoBot1Accessors.CounterStateName),
};
// ILoggerFactory を取得するため ServiceProvider を作成し、IoC より取得。
var serviceProvider = new ServiceCollection()
        .AddLogging()
        .BuildServiceProvider();
var loggingFactory = serviceProvider.GetService<ILoggerFactory>();
```

4\. テスト対象のメソッドを含むクラスをインスタンス化。

```csharp
// テスト対象のクラスをインスタンス化
var bot = new EchoBot1Bot(accessors, loggingFactory);
```

5\. TestFlow を作成してテストを設定。

```csharp
// テスト対象のクラスをインスタンス化
var bot = new EchoBot1Bot(accessors, loggingFactory);
await new TestFlow(adapter, bot.OnTurnAsync)
    .Test("foo", "Turn 1: You sent 'foo'\n")
    .StartTestAsync();
```

6\. ソリューションをビルドしてエラーがないことを確認。

最終的には以下のようなコードとなる。

```csharp
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Adapters;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Threading.Tasks;

namespace EchoBot1.UnitTest
{
    [TestClass]
    public class EchoBotUnitTest
    {
        [TestMethod]
        public async Task FirstEchoTest()
        {
            // アダプターを作成
            var adapter = new TestAdapter();
            // ステート管理のためアクセサーを作成
            IStorage dataStore = new MemoryStorage();
            var conversationState = new ConversationState(dataStore);
            var accessors = new EchoBot1Accessors(conversationState)
            {
                CounterState = conversationState.CreateProperty<CounterState>(EchoBot1Accessors.CounterStateName),
            };
            // ILoggerFactory を取得するため ServiceProvider を作成し、IoC より取得。
            var serviceProvider = new ServiceCollection()
                    .AddLogging()
                    .BuildServiceProvider();
            var loggingFactory = serviceProvider.GetService<ILoggerFactory>();

            // テスト対象のクラスをインスタンス化
            var bot = new EchoBot1Bot(accessors, loggingFactory);
            await new TestFlow(adapter, bot.OnTurnAsync)
                .Test("foo", "Turn 1: You sent 'foo'\n")
                .StartTestAsync();
        }
    }
}
```
# 動作確認

最後に動作確認を行います。

1\. EchoBot1Bot.cs コード内の OnTurnAsync メソッドにブレークポイントを配置。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/24b9f24c-4fb2-3d26-3e96-fade5f07d8ff.png)

2\. テストエクスプローラーより作成したテストをデバッグ実行。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/817db57d-f2c9-7044-541b-e71ebe3fc3f9.png)

3\. ブレークポイントがヒットすることや、テストが成功することを確認。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/2e6c6220-17dd-8231-99dc-1c9d70feb024.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/bd228ee7-dd26-6684-95ee-e6f9631b48b8.png)

# まとめ

今回はまずユニットテストをするための最小限の環境を作ってみました。次回はより本格的なボットのユニットテストを見ていきます。

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