AlexaでE2Eテストを書けるようにした話

研究開発部の伊尾木です。

研究開発部では、Alexaのスキルを公開しています(Google Assistantも公開していますよ!)。

今回はAlexaスキルのテストを便利にするKuchimaneというツールを公開したので紹介したいと思います。

E2Eテストが難しい

音声UIの開発はまだまだ新しい分野で知見やツールがそろっているわけではありません。 特に E2E (End To End) テスト、RSpecでいうところの Feature spec に相当するようなテストを行うことがとても困難でした。

AlexaでのE2Eテスト

以下のような一連の会話があったとします。

あなた「クックパッドを開いて」
Alexa「クックパッドへようこそ」
あなた「大根のレシピを教えて」
Alexa「大根ですね。サラダ、ナムル、スープのどのレシピがいいですか」
あなた「スープ」
Alexa「大根のスープですね。レシピを送信しました」

Alexaでは、この一連の会話「クックパッドを開いてから、レシピを送信するまで」をローカルでテストする方法がありません。 (Alexaのデモ環境に都度リクエストを投げればテストはできますが、やっぱりローカルだけでやりたいですよね)

会話の一部だけ、一回のやりとりだけのテストなら可能です。

例えば、「クックパッドを開いて」->「クックパッドへようこそ」 の組み合わせのみテストするといったことは可能です。 が、全部通したテストを書くことはできません。

通常のWebアプリのテストでいえば、 「一回のHTTPリクエストごとのテストは可能だけど、複数HTTPリクエスト、あるいは複数画面にまたがるテストが書けない」 という状況と同じです。

とても不便ですよね。

なぜできないのか

Alexaでは、ユーザの生の発話を、開発者が直接操作することはありません。 一旦、内部的なインテントとよばれるユーザの意図を表している処理に変換します。

例えば「クックパッドを開いて」という発話は、内部的に LaunchRequest というインテントに変換されて処理を実行します。 Webアプリとの対比でいえば、URLからコントローラ・アクション名に変換するルーティング処理と同じような感じです。

このルーティング処理が、Alexa内部に隠蔽されているため、ローカルでテストすることができないのです。 どうしてもローカルで一連の会話をテストしたい場合、ルーティング処理を自前で処理する必要があります。

Kuchimane

私達も当初、インテント単位のテストだけで乗り切ろうとしていましたが、複数インテントが絡む処理はテストできないため、エラーが起きやすい状況でした。

そこで、一連の会話をテストするための Kuchimane を開発しました!

じゃぁ Kuchimane では、さきほどのルーティング処理をどうしているのかというと、これまた自前で実装しています。

より正確にはsatori-flow というルーティング処理用のライブラリを開発しています。 KuchimaneがAlexaの会話モデル定義を解析し、このsatori-flowに「どんな発話がどのインテントになるのか」を登録します。

というわけで、以下のようなコードが書けるようになります!

const intents = { LaunchRequest, SearchDishIntent, SearchRecipeIntent };
const kuchimaneRunner = Kuchimane.runner(intents, __dirname + '/kuchimane_config.json');

it('searchRecipe', () => {
  return kuchimaneRunner.talkCheck('クックパッドを開いて', (message) => {
      expect(message).to.include('クックパッドですね')
    })()
    .then(kuchimaneRunner.talkCheck('大根のレシピを教えて', (message) => {
      expect(message).to.include('大根ですね。サラダ、ナムル、スープのどのレシピがいいですか');
    }))
    .then(kuchimaneRunner.talkCheck('スープ', (message) => {
      expect(message).to.include('大根のスープですね。レシピを送信しました');
    }))
  }
);

最初の行でintentsというオブジェクトを生成していますが、ここのLaunchRequestSearchDishIntentSearchRecipeIntent がインテント関数になります。 次にkuchimaneRunnerというインスタンスを、さきほどのintentsとKuchimane用の設定ファイル(Alexaのモデルへのパスなどを書く)から生成しています。

kuchimaneRunnertalkCheckというメソッドがE2Eテスト用のメソッドになります。第1引数がユーザの発話、第2引数がチェック用の関数になります。

talkCheckメソッドはユーザの発話を受け取ると、それを satori-flow に渡してインテント名に変換してもらいます。 そして、kuchimaneRunnerの生成時にもらったintentsの中から、インテント名にマッチする関数を取り出して実行し、Alexaのレスポンスをチェック用の関数に渡してテストを実行します。 最後にtalkCheckメソッドは、Promiseを返しますので、thenで会話を繋げていきます。

一連の会話をテストで書けることがわかりますね! 便利ですね!!

おわりに

AlexaのE2Eテストのための Kuchimane の紹介でした。

バグの多くは機能の組み合わせ部分に潜むと言われますが、実際私達も複数の会話、複数のインテントが絡む部分でよくエラーが起きていました。 Kuchimane以前は、このような部分をテストすることができなかったのですが、Kuchimaneのおかげで複数の会話が絡む部分をテストできるようになり 品質向上に一定の効果があるなと感じています。

ちなみに、まだまだKuchimaneの完成度は高くありません。例えばASK SDK v2 にも対応できていませんし、私達にとって必要な部分を優先的に実装しているため、フォローできていないケースもあります。これらの点については今後拡充していく予定です。

また現状ではGoogle Assistantに対応していませんが、こちらも今後対応する予定です!

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/