2018-07-26はじめにAlexaスキルを実装するときにもテストしながら開発したい。実機で声を出しながら実装するのは流石に効率が悪い。Alexaシミュレータを使うのも繰り返していると辛くなってくる。Lambda関数をデプロイするのも辛いし、Alexaシミュレータに発話を入力するのも面倒。なにより、エラーになったときに発生箇所がわかりにくく、デバッグログを仕込んではCloudWatchLogsで確認するのが辛い。ローカルでテストしながら開発できないものか。Alexaスキルをテストする方法を紹介する。最初に私の開発環境について、記述する。AlexaスキルのバックエンドはLambdaを利用している。ランタイムはNode8.10。ローカルのテストにおいてもNode8.10を利用している。なくても構わないが、エディタによるコード補完など便利な面が多いので、Babelでトランスパイルするくらいなら、Typescriptの利用をおすすめする。セットアップモジュールのインストールを行う。init等でプロジェクトの初期化を行ったら、次のコマンドで前節の4つと関連するモジュールをインストールする。ask-cliTypescriptの設定は次の通り。]}そして、Jestの設定は次の通り。Typescriptのトランスパイルはts-jestでおこなう。]}全体のファイル構成はこんな感じ。対話モデルを保存する。tsconfig.jsonテスト対象のスキル次のような簡単なやり取りを行うスキルをテストする例で説明する。FeelingIntentで受ける。スロットはカスタムスロットで次の様に定義している。ポイントはIDを設定しているところ。このIDはインテントハンドラーで取得することができる。テスト対象コードまずは、このスキルのエンドポイントとなるLambdaのハンドラー関数。後述するFeelingIntentHandlerと、標準インテントをリクエストハンドラとして設定している。.lambda();そしてテスト対象のFeelingIntentHandlerのコードは次の通り。ssml-builderというライブラリでアウトプットスピーチを作っているが、SSML付きのテキストを生成しているだけで、本題からそれるのでそこはスルーで。注目ポイントはカスタムスロットのIDで処理を分岐させているところ。Speech().say("まあ、元気だして。Speech().say("いつもどおりで行きましょう。Speech().say("そんなときは思い切って行動しましょう。Bespoken社によって開発されているAlexaをローカルで動かしてテストするためのライブラリ。(Alexa以外でも)Lambdaの関数をローカルで実行するには、EventやContexを用意するのが結構面倒なのだが、VirtualAlexaはAlexaから送信されてくるデータに相当するデータを生成してLambdaを実行してくれる。リクエストをLambdaにわたす前に書き換えられるフィルタや、DynamoDBやAddressAPIのモックを含んでおり、一通りのスキルの実行ができる。APIのモックもサポートする予定の様子。Spot向けのスキルのテストにも使えそうだ。よるテストコードそして、本題のテストコードは次の通り。expect(response.prompt()).toContain("元気だして。VirtualAlexaのインスタンス生成インスタンス生成時に、テスト対象のスキルのLambda関数と対話モデルのJSONを設定する。get-modelコマンドで取得できるJSONをそのままファイルに保存したもの。スロットのないサンプル発話のテストまずはスロットを含まないフレーズのテスト。VirtualAlexa#utterメソッドでテストできる。LambdaからのレスポンスのJSONがそのまま入ってくるので、そこからoutputSpeechなどを取り出して内容をチェックすることでテストできる。スロットを含むサンプル発話のテスト次に、スロットを含む発話のテスト。フレーズそのままを解釈してくれはしないので、VirtualAlexa#intendメソッドにインテント名とスロットの値を設定してテストする。responseからoutputSpeechを取り出して期待するセリフが入っているかを確認する。"絶好調"というスロットの値を取り出すのではなく、スロットの値に設定されたIDを取り出して処理を分岐させていた。テストではIDは渡さず、スロットの値として"絶好調"という文字列を渡している。インスタンス生成時に対話モデルを読み込んでいるので、VirtualAlexaがスロットとIDの対応がわかっているのでテストが可能になっている。スロットのシノニムによるテストスロットの値としてシノニムの文字列を与えた場合もちゃんとテストできる。これも、VirtualAlexaが対話モデルを元にリクエストを生成してくれているからテストできるのだとわかる。カスタムスロットにない値のテストカスタムスロットに定義していない言葉をスロット値として渡すと、当然ながらIDとの対応が取れず、もう一度聞き直す流れとなるが、それもテストできる。その他filter一つ前のやり取りを受けて、返答を変えるなど、ステートフルな実装が必要になるケースがある。セッション属性の値に応じて応答を変更するには、VirtualALexa#intentなどリクエストを発生させるメソッドを実行する前にセッション属性の値を設定したくなる。filterメソッドで設定すれば良い。virtual-alexaのインスタンスを生成した後、filterメソッドにリクエストを書き換えるコールバック関数を渡せば良い。コールバック関数にはrequestEnvelopeに相当するオブジェクトが渡されるので、その中でセッション属性を設定すれば、セッション属性に応じたハンドラの動作をテストすることができる。次の様に、beforeEachなどでfilterメソッドを呼べば良い。});});一旦、filterを設定すると、そのvirtual-alexaインスタンスでintend、utterなどリクエストを発生させるメソッドを呼ぶと、ハンドラにリクエストを渡す前にfilterに渡したコールバック関数が実行されるので注意。DynamoDBのモックVirtualAlexaはDynamoDBのモックも含んでいる。VirtualAlexaのインスタンスを生成した後、alexa.dynamoDB().mock()でモックされるようになる。.create();alexa.dynamoDB().mock();多分、不具合だと思うのだけど、次のように環境変数にAWSのリージョンを設定しておかないと接続時にエラーとなった。'ap-northeast-1';他にも、DBのテーブル名など環境変数で渡したい値と一緒に設定するコードをファイルに纏めて、setupFilesをjest.json追加して、テスト開始時に設定してしまうのが良いと思う。BeforeEachで作ればDBの状態に応じた挙動のテストも可能だ。Displayインターフェース関連のテストvirtual-alexaはDisplayインターフェース特有の実装のテストにも対応している。次の様にすると、リクエストにDisplayインターフェースサポートが追加される。後は、intendでもutterでもリクエストを実行すれば、Dipslayインターフェースサポートされている場合のテストを行うことが可能だ。ListTemplateやアクションリンクを実装すると、Display.ElementSelectedリクエストを受ける必要があるが、そのテストもサポートしている。次のようにselectElementを実行するとDisplay.ElementSelectedリクエストがハンドラに送られる。引数にはtokenの値を渡せばよい。使えば、Alexaスキル用のLambda関数をローカルで簡単に実行できる。Jestと組み合わせて使えば自動テストが実装できる。対話モデルも含めたテストが可能DynamoDBを使うスキルもモックでテストが可能となる。最後に重要なこと実機での動作確認は重要だということだ。テストできるのは次の2点に過ぎない。対話モデルが仕様どおり定義されているか否かその対話モデルに対応するLambda関数がプログラムとして意図した入力に対し意図した通り動作するかどうか。これらの前段にあるAlexaがユーザの発話を意図通り聞き取り、適切なインテントにルーティングするかはテストできない。現状では、思ったように聞き取ってくれなかったり、別のインテントにリクエストを送ってきたりすることは往々にして起こる。Alexaがしゃべるセリフの区切りやイントネーションに違和感を感じることもある。実機での動作確認を通してこれらを検出し、サンプル発話を増やしたり、Alexaが聞き取りやすい発話にユーザを誘導したり、Alexaに喋らせる言い回しを変更するなどの工夫が最終的に必要になる。