Alexaスキルのテスト、どうしてますか?色々やり方はあると思います。
- Alexa開発者コンソールのテストシミュレータ
- ベータテストを使った実機テスト
- ask dialogコマンド
- mocha/chai/jestなどによるユニットテスト
- テスト用フレームワーク
今回はVoiceflowのcommunity forumで知った「Botium」を使ってみたいと思います。
目次
Botiumとは?
Botiumはチャットボットのテスト自動化プラットフォームです。
無料プランはありますが、E2Eや音声テストなどは有償プランじゃないと使えません。有償プランの価格は載ってないですけど、ちょっと調べてみた感じ、そこそこかかるみたいですね・・・・
ただし、ツールライブラリだけを使うなら無料かつOSSになっているので、今回はそちらでやってみましょう。
Botiumのツール
Botiumのツールは以下のようになっています。
- Botium Core
- Botiumのコアライブラリ
- Botium CLI
- Botium Coreを使ったCLI。テストもできます。
- Botium Bindings
- Botium Coreとテストランナーをつなげるもの。テストランナーにはMocha/Jasmin/Jestなどが使える。Botium CLIを使わずに、自前でテストランナーをつかいた場合はこちらを選ぶようです。
- Botium Box
- BotiumのCLIやライブラリを制御するためのGUIプラットフォーム。有償。
あと、上記には書いてないですが、以下もあります。
- Botium Connector
- チャットボットやフレームワーク、プラットフォームなどとつなげるためのもの。Alexaの場合はbotium-connector-alexa-smapiを使ってつなげる。
今回使うのは以下のみです。
- botium-cli(botium core含む)
- botium-connector
Botiumのインストールと基本的な使い方
botium-cliをインストールします。
$ npm install -g botium-cli
テストプロジェクトを初期化します。ディレクトリを作成してそこで行うのが良いでしょう。
$ mkdir alexa-test-sample && cd alexa-test-sample $ botium-cli init Botium Configuration File written to "./botium.json". Botium Convo File written to "/path-to-somewhere/alexa-test-sample/give_me_a_picture.convo.txt". Botium initialization ready. You should now run "botium-cli run --verbose --convos ." to verify.
botium-cli init
を実行するとテストの雛形が作成されます。
$ tree . ├── botium.json └── give_me_a_picture.convo.txt
botium.json
にテストプロジェクトの設定を記載します。デフォルトで用意されているテスト(botium-connector-echo)の設定が行われているようです。
{ "botium": { "Capabilities": { "PROJECTNAME": "My Botium Project", "CONTAINERMODE": "echo" }, "Sources": {}, "Envs": {} } }
.convo.txt
がテストシナリオを記載するファイルになります。botium-cliは実行時に現在のディレクトリから*.convo.txtを探してテストを行うようになっているようですね。こちらもデフォルトで用意されているテスト用のシナリオが記載されているようですが、見て分かる通り、mochaやjestなどと違って、非常に簡易かつわかりやすい感じの記述になっていますね。
give me picture #me Hello, Bot! #bot You said: Hello, Bot! #me give me a picture #bot Here is a picture MEDIA logo.png
では早速実行してみましょう。
$ botium-cli run --verbose --convos botium-cli Using Botium configuration file ./botium.json +0ms botium-cli-run command options: { botium-cli-run _: [ 'run' ], botium-cli-run verbose: true, botium-cli-run v: true, botium-cli-run convos: [ '.' ], botium-cli-run C: [ '.' ], botium-cli-run config: './botium.json', botium-cli-run c: './botium.json', botium-cli-run output: 'spec', botium-cli-run testsuitename: 'Botium Test-Suite', botium-cli-run n: 'Botium Test-Suite', botium-cli-run expandutterances: false, botium-cli-run expandscriptingmemory: false, botium-cli-run timeout: 60, botium-cli-run '$0': '../../.volta/tools/image/packages/botium-cli/bin/botium-cli' botium-cli-run } +0ms botium-cli-run Mocha Reporter "spec", options: undefined +0ms botium-core-BotDriver Loaded Botium configuration files /path-in-somewhere/alexa-test/botium.json +0ms botium-core-ScriptingProvider ReadConvosFromDirectory(.) found filenames: botium.json,give_me_a_picture.convo.txt +0ms botium-core-ScriptingProvider ReadConvosFromDirectory(.) found convos: botium-core-ScriptingProvider 1 give me picture ({ convoDir: '.', filename: 'give_me_a_picture.convo.txt' }): Line 3: #me - Hello, Bot! | Line 6: #bot - You said: Hello, Bot! | Line 9: #me - give me a picture | Line 12: #bot - Here is a picture MEDIA(logo.png) +4ms botium-core-ScriptingProvider ReadConvosFromDirectory(.) found utterances: botium-core-ScriptingProvider none +1ms botium-core-ScriptingProvider ReadConvosFromDirectory(.) found partial convos: botium-core-ScriptingProvider none +0ms botium-core-ScriptingProvider ReadConvosFromDirectory(.) scripting memories: botium-core-ScriptingProvider none +0ms botium-cli-run ready reading convos (1), expanding convos ... +238ms botium-core-ScriptingProvider ExpandConvos - Using utterances expansion mode: all +0ms botium-cli-run ready expanding convos and utterances, number of test cases: (1). +23ms botium-cli-run adding test case give me picture (from: { convoDir: '.', filename: 'give_me_a_picture.convo.txt' }) +2ms Botium Test-Suite botium-core-BotDriver Build - Botium Core Version: 1.11.15 +280ms botium-core-BotDriver Build - Capabilites: { botium-core-BotDriver PROJECTNAME: 'My Botium Project', botium-core-BotDriver TESTSESSIONNAME: 'Botium Test Session', botium-core-BotDriver TESTCASENAME: 'Botium Test Case', botium-core-BotDriver TEMPDIR: 'botiumwork', botium-core-BotDriver CLEANUPTEMPDIR: true, botium-core-BotDriver WAITFORBOTTIMEOUT: 10000, botium-core-BotDriver SIMULATE_WRITING_SPEED: false, botium-core-BotDriver SIMPLEREST_PING_RETRIES: 6, botium-core-BotDriver SIMPLEREST_PING_TIMEOUT: 10000, botium-core-BotDriver SIMPLEREST_PING_VERB: 'GET', botium-core-BotDriver SIMPLEREST_PING_UPDATE_CONTEXT: true, botium-core-BotDriver SIMPLEREST_PING_PROCESS_RESPONSE: false, botium-core-BotDriver SIMPLEREST_INIT_PROCESS_RESPONSE: false, botium-core-BotDriver SIMPLEREST_STOP_RETRIES: 6, botium-core-BotDriver SIMPLEREST_STOP_TIMEOUT: 10000, botium-core-BotDriver SIMPLEREST_STOP_VERB: 'GET', botium-core-BotDriver SIMPLEREST_START_RETRIES: 6, botium-core-BotDriver SIMPLEREST_START_TIMEOUT: 10000, botium-core-BotDriver SIMPLEREST_START_UPDATE_CONTEXT: true, botium-core-BotDriver SIMPLEREST_START_PROCESS_RESPONSE: true, botium-core-BotDriver SIMPLEREST_START_VERB: 'GET', botium-core-BotDriver SIMPLEREST_POLL_VERB: 'GET', botium-core-BotDriver SIMPLEREST_POLL_INTERVAL: 1000, botium-core-BotDriver SIMPLEREST_POLL_UPDATE_CONTEXT: true, botium-core-BotDriver SIMPLEREST_METHOD: 'GET', botium-core-BotDriver SIMPLEREST_IGNORE_EMPTY: true, botium-core-BotDriver SIMPLEREST_TIMEOUT: 10000, botium-core-BotDriver SIMPLEREST_EXTRA_OPTIONS: {}, botium-core-BotDriver SIMPLEREST_STRICT_SSL: true, botium-core-BotDriver SIMPLEREST_INBOUND_UPDATE_CONTEXT: true, botium-core-BotDriver SIMPLEREST_CONTEXT_MERGE_OR_REPLACE: 'MERGE', botium-core-BotDriver SCRIPTING_TXT_EOL: '\n', botium-core-BotDriver SCRIPTING_XLSX_EOL_WRITE: '\r\n', botium-core-BotDriver SCRIPTING_XLSX_HASHEADERS: true, botium-core-BotDriver SCRIPTING_CSV_SKIP_HEADER: true, botium-core-BotDriver SCRIPTING_CSV_QUOTE: '"', botium-core-BotDriver SCRIPTING_CSV_ESCAPE: '"', botium-core-BotDriver SCRIPTING_NORMALIZE_TEXT: true, botium-core-BotDriver SCRIPTING_ENABLE_MEMORY: false, botium-core-BotDriver SCRIPTING_ENABLE_MULTIPLE_ASSERT_ERRORS: false, botium-core-BotDriver SCRIPTING_MATCHING_MODE: 'wildcardIgnoreCase', botium-core-BotDriver SCRIPTING_UTTEXPANSION_MODE: 'all', botium-core-BotDriver SCRIPTING_UTTEXPANSION_RANDOM_COUNT: 1, botium-core-BotDriver SCRIPTING_UTTEXPANSION_NAMING_MODE: 'justLineTag', botium-core-BotDriver SCRIPTING_UTTEXPANSION_NAMING_UTTERANCE_MAX: '16', botium-core-BotDriver SCRIPTING_MEMORYEXPANSION_KEEP_ORIG: false, botium-core-BotDriver SCRIPTING_FORCE_BOT_CONSUMED: false, botium-core-BotDriver ASSERTERS: [], botium-core-BotDriver LOGIC_HOOKS: [], botium-core-BotDriver USER_INPUTS: [], botium-core-BotDriver SECURITY_ALLOW_UNSAFE: true, botium-core-BotDriver CONTAINERMODE: 'echo', botium-core-BotDriver CONFIG: './botium.json' botium-core-BotDriver } +0ms botium-core-BotDriver Build - Sources : { LOCALPATH: '.', GITPATH: 'git', GITBRANCH: 'master', GITDIR: '.' } +1ms botium-core-BotDriver Build - Envs : { IS_BOTIUM_CONTAINER: true } +0ms botium-connector-PluginConnectorContainer-helper Botium plugin botium-connector-echo loaded. Plugin version is 0.0.16 +0ms botium-cli-run running testcase give me picture +56ms botium-core-Convo give me picture/Line 3: user says (cleaned by binary and base64 data and sourceData) { botium-core-Convo "sender": "me", botium-core-Convo "channel": null, botium-core-Convo "not": false, botium-core-Convo "optional": false, botium-core-Convo "messageText": "Hello, Bot!", botium-core-Convo "media": null, botium-core-Convo "buttons": null, botium-core-Convo "cards": null, botium-core-Convo "forms": null, botium-core-Convo "attachments": null, botium-core-Convo "asserters": [], botium-core-Convo "userInputs": [], botium-core-Convo "logicHooks": [] botium-core-Convo } +0ms botium-connector-echo UserSays called, echo back +0ms botium-core-Convo give me picture wait for bot +1ms botium-core-Convo give me picture: bot says (cleaned by binary and base64 data and sourceData) { botium-core-Convo "sender": "bot", botium-core-Convo "messageText": "You said: Hello, Bot!", botium-core-Convo "channel": "default" botium-core-Convo } +1ms botium-core-ScriptingMemory fill start: {} +0ms botium-core-ScriptingProvider assertBotResponse give me picture/Line 6 (Line 3: #me - Hello, Bot!) BOT: You said: Hello, Bot! = You said: Hello, Bot! ... +86ms botium-core-Convo give me picture/Line 9: user says (cleaned by binary and base64 data and sourceData) { botium-core-Convo "sender": "me", botium-core-Convo "channel": null, botium-core-Convo "not": false, botium-core-Convo "optional": false, botium-core-Convo "messageText": "give me a picture", botium-core-Convo "media": null, botium-core-Convo "buttons": null, botium-core-Convo "cards": null, botium-core-Convo "forms": null, botium-core-Convo "attachments": null, botium-core-Convo "asserters": [], botium-core-Convo "userInputs": [], botium-core-Convo "logicHooks": [] botium-core-Convo } +2ms botium-connector-echo UserSays called, echo back +3ms botium-core-Convo give me picture wait for bot +0ms botium-core-Convo give me picture: bot says (cleaned by binary and base64 data and sourceData) { botium-core-Convo "sender": "bot", botium-core-Convo "messageText": "Here is a picture", botium-core-Convo "media": [ botium-core-Convo { botium-core-Convo "altText": "Botium Logo", botium-core-Convo "mediaUri": "https://www.botium.ai/wp-content/uploads/2020/03/logo.png" botium-core-Convo } botium-core-Convo ], botium-core-Convo "nlp": { botium-core-Convo "intent": { botium-core-Convo "name": "picture", botium-core-Convo "confidence": 0.8 botium-core-Convo } botium-core-Convo }, botium-core-Convo "channel": "default" botium-core-Convo } +1ms botium-core-ScriptingMemory fill start: {} +3ms botium-core-ScriptingProvider assertBotResponse give me picture/Line 12 (Line 9: #me - give me a picture) BOT: Here is a picture = Here is a picture ... +3ms botium-cli-run give me picture ready, calling done function. +10ms ✔ give me picture botium-connector-BaseContainer Cleanup rimrafing temp dir /Users/kun432/repository/alexa-test/botiumwork/My Botium Project 20220321 223340 fLHQW +0ms 1 passing (54ms)
いろいろ出力されますが、最後に1 passing
と表示されれば、convo.txtのシナリオ通りにボットとの会話が行われたということになります。
これでサンプルのテスト雛形は不要なのですべて削除してください。
$ rm -rf *
Alexaスキル向けテストプロジェクトの作成
Alexa用のbotium connectorをインストールします。
$ npm install -g botium-connector-alexa-smapi
Alexaスキル向けテストプロジェクトの初期化を行います。
$ botium-connector-alexa-smapi-cli init
ここから対話形式で設定を行います。画面の説明に従って設定を行えばOKです。順に行きましょう。
This wizard will guide you through the Botium Connector setup. Please follow the instructions. It involves Copy&Paste from a web browser to this terminal window. ######## Step 1/3 - Create Amazon Security Profile ######## 1. Go to this url: https://developer.amazon.com/home.html 2. Open "Settings" / "Security Profiles" and create a new profile or select an existing one 3. Add this url to the "Allowed Return URLs": https://s3.amazonaws.com/ask-cli/response_parser.html Copy & Paste the "Client-ID" here:
ターミナルを一旦このままにしておいて、https://developer.amazon.com/home.htmlにアクセス、ログインして、「設定」→「セキュリティプロファイル」と進み、既存のセキュリティプロファイルを選択するか、セキュリティプロファイルを新規作成します。ここでは新規に作成してみましょう。
セキュリティプロファイル名と説明を適当に記載して、保存します。
セキュリティプロファイルが作成されたら、「ウェブ設定」のタブをクリックします。
右下の編集をクリックします。
「許可された返信URL 」に先ほどターミナルに表示されていたURL(https://s3.amazonaws.com/ask-cli/response_parser.html)を入力して、保存します。
次に、セキュリティプロファイルのウェブ設定で表示されている「クライアントID」をコピーして、
ターミナルに貼り付けてENTER。
Copy & Paste the "Client-ID" here: amzn1.application-oa2-client.XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
次にクライアントシークレットについて聞かれます。
Copy & Paste the "Client-Secret" here:
「シークレットを表示」をクリック
え
表示された「クライアントシークレット」をコピーして、
ターミナルに貼り付けてENTER。
Copy & Paste the "Client-Secret" here: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
次に表示されているURLにアクセスします。
######## Step 2/3 - Get Amazon Authorization Code ######## 1. Paste the following url to your browser and follow the instructions: https://www.amazon.com/ap/oa?response_type=code&client_id=amzn1.application-oa2-client.(...snip...) Copy&Paste the Authorization Code you received:
Alexa開発者アカウント(amazon.co.jpアカウント)でログインします。
権限を聞かれるので許可します。
認証コードが生成されますので、これをコピーして、
ターミナルに貼り付けてENTER。
Copy&Paste the Authorization Code you received: XXXXXXXXXXXXXXXXX
Alexa開発者コンソール上で作成しているスキルが一覧表示されますので、テストを行うスキルの番号を入力します。今回はテスト用に用意したスキルを選択しました。
######## Step 3/3 - Selecting vendor id and skill ######## Using vendor id "XXXXXXXXXXXXXX" (hoge) for your account Found 50 skills for your account 1: "テストスキル1" (amzn1.ask.skill.XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXX1) 2: "テストスキル2" (amzn1.ask.skill.XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXX2) ...snip... 19: "トリップアドバイザー" (amzn1.ask.skill.XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXX3) ...snip... Enter Number of skill to use: 19
以下のように表示されればOKです。入力した情報をもとにbotium.jsonが生成されます。
######## Ready - Creating botium.json ######## Done.
中身を少し見てみましょう。
{ "botium": { "Capabilities": { "CONTAINERMODE": "alexa-smapi", "ALEXA_SMAPI_REFRESHTOKEN": "XXX(...snip...)XXX", "ALEXA_SMAPI_CLIENTID": "amzn1.application-oa2-client.XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", "ALEXA_SMAPI_CLIENTSECRET": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", "ALEXA_SMAPI_VENDORID": "XXXXXXXXXXXXXX", "ALEXA_SMAPI_SKILLID": "amzn1.ask.skill.XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXX3" } } }
CONTAINERMODE
がalexa-smapi
になっていて、対話形式で入力したものが設定されているのがわかると思います。少しだけ追加します。
{ "botium": { "Capabilities": { "PROJECTNAME": "トリップアドバイザー", // 追加 "ALEXA_SMAPI_LOCALE": "ja-JP", // 追加 "CONTAINERMODE": "alexa-smapi", "ALEXA_SMAPI_REFRESHTOKEN": "XXX(...snip...)XXX", "ALEXA_SMAPI_CLIENTID": "amzn1.application-oa2-client.XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", "ALEXA_SMAPI_CLIENTSECRET": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", "ALEXA_SMAPI_VENDORID": "XXXXXXXXXXXXXX", "ALEXA_SMAPI_SKILLID": "amzn1.ask.skill.XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXX3" } } }
PROJECTNAME
はテストプロジェクトの名前です。これはあってもなくてもいいです。ALEXA_SMAPI_LOCALE
はスキルのロケールです。これが定義されていない場合、どうやらen-US
でテストが行われ、en-US
向けにスキルが作成されていない場合は失敗します。日本語の場合はja-JP
を設定しておきます。なお、説明のためにコメントを入れていますが、JSONではコメントが使えないので実際に設定する際には入れないようにしてください。
Botiumを使ったAlexaスキルのテスト
ではテストシナリオを作りましょう。その前にサンプルのスキルの会話フローをご紹介します。今回のサンプルはVoiceflowで作成しています。
行きたい都市名を言うと、おすすめの観光地を教えてくれるというものです。実際に作る場合はもっと複雑になると思いますが、テストのためのサンプルなので。ハッピーパスはこういう感じになります。
ではテストシナリオです。tripadvisor_happypath01.convo.txtという名前でシナリオを作りました。*.convo.txtであればなんでもよいです。
トリップアドバイザーのテスト:Happy Path #1 #me トリップアドバイザーを開いて #bot はじめまして、トリップアドバイザースキルをご利用いただきありがとうございます。このスキルでは行きたい日本の都市名をいうとおすすめの観光名所を提案します。例えば「京都に行きたい」と言ってみてください。 #me 京都 かな #bot 京都 ですね。京都 は清水寺がおすすめです。
注意する点としては、スロットおよび変数の前後には半角スペースが必要になります。これはVoiceflowの仕様によるかもしれません(Voiceflowでは必ず必要になる。通常のAlexaスキルだと不要かも)
ではテストを実行してみましょう。
$ botium-cli run Botium Test-Suite ✔ トリップアドバイザーのテスト:Happy Path #1 (10767ms) 1 passing (12s)
はい、問題なくテスト成功していますね。
失敗の場合も見てみましょう。シナリオファイルを少し修正します。
#bot 京都 ですね。京都 は東大寺がおすすめです。
実際にはシナリオ側が間違っているのですが、エラーの確認のため、ということで。実行してみましょう。
$ botium-cli run Botium Test-Suite 1) トリップアドバイザーのテスト:Happy Path #1 0 passing (12s) 1 failing 1) Botium Test-Suite トリップアドバイザーのテスト:Happy Path #1: Error: トリップアドバイザーのテスト:Happy Path #1/Line 12: Bot response (on Line 9: #me - 京都 かな) "京都 ですね。京都 は清水寺がおすすめです。" expected to match "京都 ですね。京都 は東大寺がおすすめです。" ######################################## ASSERTION FAILED in TextMatchAsserter - Expected: ["京都 ですね。京都 は東大寺がおすすめです。"] - Actual: "京都 ですね。京都 は清水寺がおすすめです。" INPUT: 京都 かな ------------ TRANSCRIPT ---------------------------- #me: トリップアドバイザーを開いて #bot: <speak>はじめまして、トリップアドバイザースキルをご利用いただきありがとうございます。このスキルでは行きたい日本の都市名をいうとおすすめの観光名所を提案します。例えば「京都に行きたい」と言ってみてください。</speak> #me: 京都 かな #bot: <speak>京都 ですね。京都 は清水寺がおすすめです。</speak> at wrapBotiumError (/path-in-somewhere/botium-cli/lib/node_modules/botium-cli/src/run/index.js:76:12) at finish (/path-in-somewhere/botium-cli/lib/node_modules/botium-cli/src/run/index.js:205:24) at /path-in-somewhere/botium-cli/lib/node_modules/botium-cli/src/run/index.js:218:11
はい、想定とテスト結果が異なっていることがわかりますね。このようにしてテストを行えばよいわけです。
また、テストファイルは複数用意することができます。同じパスに以下のファイルを追加します。
tripadvisor_happypath02.convo.txt
トリップアドバイザーのテスト:Happy Path #2 #me トリップアドバイザーを開いて #bot はじめまして、トリップアドバイザースキルをご利用いただきありがとうございます。このスキルでは行きたい日本の都市名をいうとおすすめの観光名所を提案します。例えば「京都に行きたい」と言ってみてください。 #me 奈良 に行きたい #bot 奈良 ですね。奈良 は東大寺がおすすめです。
tripadvisor_nomatch_city.convo.txt
トリップアドバイザーのテスト:都市名該当なし #me トリップアドバイザーを開いて #bot はじめまして、トリップアドバイザースキルをご利用いただきありがとうございます。このスキルでは行きたい日本の都市名をいうとおすすめの観光名所を提案します。例えば「京都に行きたい」と言ってみてください。 #me 神戸 #bot ごめんなさい、神戸 はまだ対応していません。
では実行してみましょう。
$ botium-cli run Botium Test-Suite ✔ トリップアドバイザーのテスト:Happy Path #1 (10344ms) ✔ トリップアドバイザーのテスト:Happy Path #2 (10557ms) ✔ トリップアドバイザーのテスト:都市名該当なし (7728ms) 3 passing (29s)
こんな感じでまとめてテストを実行してくれます。
まとめ
Botiumのシナリオはとても簡易でわかりやすいですね。mocha/jestなどは多少なりともコードの書き方を覚えておく必要がありますが、Botiumなら開発者じゃなくてもテスト用のシナリオをかけそうです。
とはいえ、これだけだと複雑なテストを書くには足りないですね。ということで、次回はシナリオの書き方についていろいろ見ていきたいと思います。