alexa-conversationを使ってAlexaのローカルテストをしてみる

alexa-eyecatch

灼熱の候, 真っ青な空に入道雲が湧き上がる盛夏の季節になりましたが、いかがお過ごしでしょうか、せーのです。
今日はなにかとやりにくいAlexaのテストについてのツールをご紹介します。

テストは大事

AlexaのCustom Skillを作成したことがある方はわかると思いますが、音声アシスタントを使用した音声認識ソリューションにも関わらず、開発手法は通常のシステム開発とほぼほぼ変わらないです。設計をし、コードを書き、テストをして、リリースする。入力や出力のインターフェースが音声だとしてもこの流れは変わらないです。

そうなると、やはりテストコードを書いて品質を担保したくなる、というのが人情というもの。今回はこのAlexa用のテストコードを簡単にかけるnodeモジュールをご紹介します。名前を「alexa-conversation」と言います。

alexa-conversation

alexa-conversationはmochaをベースに組まれているAlexa用のテストモジュールです。テストできる範囲はCustom Skillとして実装しているLambdaの中、となります。IntentやSlotを指定するとどのような返答がAlexaから返ってくるのか、というE2Eに近い部分を"conversation"の名の通り「会話形式」でかけます。このコードがPASSすれば「正しくIntentが呼ばれていれば要望している回答が返ってくる」ということが担保出来ます。
もちろんチェーンで書くことも可能なので「コーヒーをちょうだい」「種類は何にしますか」「モカで」「モカですね。砂糖はいくつにしますか?」「3つで」「モカで砂糖3つですね。クッキーとかは要らないですか?」というような一連の会話の流れもテストすることができます。

const conversation = require('alexa-conversation');
const app = require('../../index.js'); // your Alexa skill's main file. 
 
const opts = { // those will be used to generate the requests to your skill 
  name: 'Test Conversation',
  appId: 'your-app-id',
  // Either provide your app (app.handler must exist)... 
  app: app,
  // ...or pass the handler in directly (for example, if you have a custom handler name) 
  handler: app.customHandlerName
  // Other optional parameters. See readme.md 
};
 
// initialize the conversation 
conversation(opts)
  .userSays('LaunchIntent') // trigger the first Intent 
    .plainResponse // this gives you access to the non-ssml response 
        // asserts that response and reprompt are equal to the given text 
      .shouldEqual('Welcome back', 'This is the reprompt')
        // assert not Equals 
      .shouldNotEqual('Wrong answer', 'Wrong reprompt')
 	    // assert that repsonse contains the text 
      .shouldContain('Welcome')
  	  // assert that the response matches the given Regular Expression 
      .shouldMatch(/Welcome(.*)back/)
        // fuzzy match, not recommended for production use. See readme.md for more details 
      .shouldApproximate('This is an approximate match')
  .userSays('IntentWhichRequiresSlots', {slotOne: 'slotValue'}) // next interaction, this time with a slot. 
    .ssmlResponse // access the SSML response 
      .shouldMatch(/<say>(Hello|Bye)</say>/)
      .shouldNotMatch(/<say>Wrong answer</say>/)
  .end(); // this will actually run the conversation defined above 

テスト内容

Intentに対する回答の書き方は

  • shouldEqual
  • shouldNotEqual
  • shouldContain
  • shouldMatch
  • shouldNotMatch
  • shouldApproximate

があります。それぞれ

  • shouldEqual: 完全一致
  • shouldNotEqual: 一致しない
  • shouldContain: 前方/後方一致
  • shouldMatch: 正規表現で一致する
  • shouldNotMatch: 正規表現で一致しない
  • shouldApproximate: 近似マッチング

を表します。最初はContainやMatchで何となくの会話の流れを定義しておいて、実装後細かいセリフが決まり次第shouldEqualに変えていく、というのが定石でしょうか。

やってみる

それではやってみましょう。まずはalexa-conversationをインストールします。インストールはnpmになり、ベースライブラリとしてmochaが必要です。nodeのバージョンは5.0.0以上、mochaは3.0.0が必要です。package.jsonがあるのでNodeとnpmさえある程度のバージョンになっていれば、あとは自動で入れてくれます。nodeやnpmのバージョンをあげるには「n」というモジュールが便利です。

n v6.0.0
npm update -g npm
npm install --save-dev alexa-conversation
npm install -g mocha 
npm install alexa-sdk

mkdir test
vi test-conversation.js

それでは書いていきましょう。今回はこんなシナリオを用意しました。

ユーザ: Hello.
Alexa: Hi. What's your name?
ユーザ: My name is ◯◯.
Alexa: Nice to meet you, ◯◯. Enjoy Alexa world!

ユーザのセリフは userSays で、Alexaの答えは plainResponse ssmlResponse があります。SSMLを使って抑揚やスピードを調節している時はssmlResponseを使うのですね。今回はシンプルにplainResponseを使います。


const conversation = require('alexa-conversation');
const app = require('../index.js'); 
 
const opts = { 
  name: 'HelloAlexa Test',
  appId: 'your-app-id',
  app: app,
  handler: app.customHandlerName
};
 

conversation(opts)
  .userSays('HelloIntent') 
    .plainResponse 
      .shouldContain("Hi. What's your name?")
  .userSays('NameIntent', {NameSlot: 'Tsuyoshi'}) 
    .plainResponse 
      .shouldContain("Nice to meet you, Tsuyoshi. Enjoy Alexa world!")  	 
  .end();
 

最初の「Hello」は"HelloIntent"、次の「My name is ◯◯」は"NameIntent"という名前にしました。 次にテスト対象となるlambdaのガワだけ書いていきます。

var Alexa = require('alexa-sdk');

var handlers = {
    'LaunchRequest': function () {
        this.emit(':tell', 'Hello');
    },

    'HelloIntent': function () {
        this.emit(':tell', 'Hello');
    },

    'NameIntent': function () {
        this.emit(':tell', 'Hello');
    },

 };

exports.handler = function(event, context, callback) {
    var alexa = Alexa.handler(event, context, callback);
    alexa.registerHandlers(handlers);
    alexa.execute();
};

ひたすら、何を聴かれても「Hello」という答えを返すストイックなスキルです。これでひとまずテストを走らせます。

mocha test/test-conversation.js

  Warning: Application ID is not set
  Executing conversation: HelloAlexa Test
    ✓ Finished executing conversation

  Conversation: HelloAlexa Test
    User triggers: HelloIntent
      1) Alexa's plain text response should contain: Hi. What's your name?
    User triggers: NameIntent SLOTS: {NameSlot: Tsuyoshi}
      2) Alexa's plain text response should contain: Nice to meet you, Tsuyoshi. Enjoy Alexa world!


  1 passing (19ms)
  2 failing

  1) Conversation: HelloAlexa Test User triggers: HelloIntent  Alexa's plain text response should contain: Hi. What's your name?:
     AssertionError: expected ' Hello ' to include 'Hi. What\'s your name?'
      at Function.assert.include (node_modules/alexa-conversation/node_modules/chai/lib/chai/interface/assert.js:843:45)
      at Context.it (node_modules/alexa-conversation/response.js:100:32)

  2) Conversation: HelloAlexa Test User triggers: NameIntent SLOTS: {NameSlot: Tsuyoshi} Alexa's plain text response should contain: Nice to meet you, Tsuyoshi. Enjoy Alexa world!:
     AssertionError: expected ' Hello ' to include 'Nice to meet you, Tsuyoshi. Enjoy Alexa world!'
      at Function.assert.include (node_modules/alexa-conversation/node_modules/chai/lib/chai/interface/assert.js:843:45)
      at Context.it (node_modules/alexa-con

あたりまえですがテストには失敗しました。ここから仕様に合うように実装をつけていきます。

var Alexa = require('alexa-sdk');

var handlers = {
    'LaunchRequest': function () {
        this.emit(':ask', "Welcome to Alexa test.");
    },

    'HelloIntent': function () {
        this.emit(':ask', "Hi. What's your name?");
    },

    'NameIntent': function () {
    	const NameSlotFilled = this.event.request.intent && this.event.request.intent.slots && this.event.request.intent.slots.NameSlot && this.event.request.intent.slots.NameSlot.value;

    	if (!NameSlotFilled) {
    		this.emit(':tell', "I don't know your name, sorry.");
    	} else {
    		this.emit(':tell', 'Nice to meet you, ' + NameSlotFilled + '. Enjoy Alexa world!');
    	}
        
    },

 };

exports.handler = function(event, context, callback) {
    var alexa = Alexa.handler(event, context, callback);
    alexa.registerHandlers(handlers);
    alexa.execute();
};

それではもう一度テストを回してみましょう。

mocha test/test-conversation.js


Warning: Application ID is not set
Warning: Application ID is not set
  Executing conversation: HelloAlexa Test
    ✓ Finished executing conversation

  Conversation: HelloAlexa Test
    User triggers: HelloIntent
      ✓ Alexa's plain text response should contain: Hi. What's your name?
    User triggers: NameIntent SLOTS: {NameSlot: Tsuyoshi}
      ✓ Alexa's plain text response should contain: Nice to meet you, Tsuyoshi. Enjoy Alexa world!


  3 passing (15ms)

今度はテストが通りました。あとはAlexaのDeveloperコンソールでこのIntentやSlotを適切に叩けるような設定をすればOK、となります。

注意点

見て頂いてわかるように、これはあくまでCustom Skillを書いているLambdaのテストツールとなります。このLambdaがこの形で叩けるためには適切なIntentの設定やSample Utteranceの量が必要になります。そしてシミュレータやEchoの実機などで実際に音声でテストすることを忘れないようにしましょう。

まとめ

以上、AlexaのCustom Skillに対するテスト方法をご紹介しました。VUIはとかく複雑な仕様になりがちです。コード上のバグはなるべく減らすようにこのようなテストモジュールを活用していきましょう。