ASK SDK v2 for Node.jsのテストフレームワークが欲しい(作ってみた)


はじめに

先月の4/18にAlexa Skills Kit SDK v2 for Node.jsが提供されました。

今まで機能テストではalexa-conversationというテストフレームワークにお世話になっていたのですが、
v2には対応していない為、マイグレーションを考えていく上では対応を期待したいところでした。

が、半年以上更新が止まったままなので今後も動きは無いかもしれません。。。

テストフレームワークが無くても実機でテストを行えば良い話なのですが、
以前の投稿で書いたようにローカルで気軽にテストがしたいんです!

というわけで以下のような理由(一番の理由は3つめ)で自分で作ってみました!
もちろん実装はTypeScriptです!

  1. ローカルで気軽にテストがしたい
  2. マイグレーションには早めに着手したい
  3. とりあえずOSS開発してみたい

目次

  • テストフレームワークの作成
  • 公開の準備
    • npm設定
    • npmへのユーザ登録
    • package.json設定
    • .npmignore作成
  • 公開
  • まとめ

テストフレームワークの作成

アサーションについて

alexa-conversationのアサーションには、以下のものがありました。

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

作成したフレームワークでは、以下の機能を提供することにしました。
種類は増えてしまいましたが、視覚的に何を評価しているのかがコードから判別しやすいといいなという思いです。

関数名 概要
equalPlain(expected: IOutputSpeech) プレーンテキストが完全一致すること
equalSsml(expected: IOutputSpeech) SSMLテキストが完全一致すること
notEqualPlain(expected: IOutputSpeech) プレーンテキストが完全一致しないこと
notEqualSsml(expected: IOutputSpeech) SSMLテキストが完全一致しないこと
containsPlain(expected: IOutputSpeech) プレーンテキストが部分一致すること
containsSsml(expected: IOutputSpeech) SSMLテキストが部分一致すること
matchesPlain(expected: IOutputSpeech) プレーンテキストが正規表現と一致すること
matchesSsml(expected: IOutputSpeech) SSMLテキストが正規表現と一致すること
doesNotMatchPlain(expected: IOutputSpeech) プレーンテキストが正規表現と一致しないこと
doesNotMatchSsml(expected: IOutputSpeech) SSMLテキストが正規表現と一致しないこと
startsWithPlain(expected: IOutputSpeech) プレーンテキストが前方一致すること
startsWithSsml(expected: IOutputSpeech) SSMLテキストが前方一致すること
endsWithPlain(expected: IOutputSpeech) プレーンテキストが後方一致すること
endsWithSsml(expected: IOutputSpeech) SSMLテキストが後方一致すること

IOutputSpeech インタフェース

検証する会話の内容を設定します。
現在対応しているものは以下の4つです。

  • speech - Alexaが返答する最初の発話
  • reprompt - 一定時間応答が無かった場合の発話
  • cardTitle - カードタイトル
  • cardContent - カード内容

今後はカード情報も検証できるよう拡張していく予定です。
カードタイトル、カード内容も追加しました。

サンプル

サンプルは、githubにもありますが以下のような感じです。
呼出したインテントに合わせて結果を確認するようにメソッドチェインしていきます。

sample
import * as Conversation from 'alexa-conversation-model-assert';
import { handler } from '../src/index';

const condition: Conversation.IConversationCondition = {
  handler: handler,
  request: {
    locale: 'ja-JP'
  },
  testDescription: 'hello-world'
};

const scenario = Conversation.init(condition);

/**
 * シナリオテスト
 */
scenario
  .requestIntent('LaunchRequest')
  .equalPlain({
    speech: 'Welcome to the Alexa Skills Kit, you can say hello!',
    reprompt: 'Welcome to the Alexa Skills Kit, you can say hello!'
  })
  .requestIntent('AMAZON.HelpIntent')
  .equalPlain({
    speech: 'You can say hello to me!',
    reprompt: 'You can say hello to me!'
  })
  .requestIntent('AMAZON.StopIntent')
  .equalPlain({
    speech: 'Goodbye!'
  })
  .equalSsml({
    speech: '<speak>Goodbye!</speak>'
  })
  .end();

利用を開始する為にはまずinitを呼出し、初期化します。

初期化時の条件

export interface IConversationCondition {
  handler: LambdaHandler;
  request?: {
    locale?: string;
  };
  testDescription: string;
  isEnabledTrace?: boolean;
}
項目 説明
handler Lambdaハンドラー
request.locale ロケール設定(ja-JP, en-USなど)
testDescription テストの概要を入力
isEnabledTrace テストフレームワーク側の任意の箇所にトレース出力を仕込んでます。
出力を有効にする場合、trueを設定。

requestIntentの引数

requestIntentの引数は、

export interface IConversationBuilder {
    requestIntent(intentName: string, condition?: IRequestIntentCondition): this;
    // 省略
}
項目 説明
intentName 呼び出すインテント名を指定
condition インテント呼出の条件(後述)
IRequestIntentCondition.ts
export interface IRequestIntentCondition {
    context?: {
        System?: {
            application?: AskModel.Application;
            device?: AskModel.Device;
            user?: AskModel.User;
            apiAccessToken?: string;
        };
    };
    request?: {
        dialogState?: AskModel.DialogState;
        intent?: {
            confirmationStatus?: AskModel.IntentConfirmationStatus;
            slots?: {
                [key: string]: AskModel.Slot
            }
        };
        requestId?: string;
    };
    session?: {
        newSession?: boolean;
        sessionId?: string;
        application?: AskModel.Application;
        user?: AskModel.User;
    };
    slots?: {
        [key: string]: AskModel.Slot;
    };
}

渡された条件を基に、ask-sdk-modelRequestオブジェクトの中身を設定します。
slotsのみ指定しやすいように上の階層に移動しています。
(本来の設定場所は、request.intent.slots

↑20180520追記
指定しやすいように外出しましたが、実際自分で使ってみて元の位置で良いなという結論になったので
将来的に↑は非推奨となります。
request.intent.slotsから指定してください。

スロットを指定する場合は例えば以下のように設定します。

  .requestIntent('RecipeIntent', {
    request: {
      intent: {
        slots: {
          Item: {
            name: 'Item',
            value: 'slot sample',
            confirmationStatus: 'CONFIRMED'
          }
        }
      }
    }
  })

公開の準備

npmjsへのサインアップ

以下の情報を入力しサインアップしましょう。
設定例として私が入力したものを載せています。

項目 設定例 備考
Full Name Daisuke Araki
Public Email [email protected] 公開可能なメールアドレス
UserName daisukeark
Password 任意

こんな感じで出てきます。

設定

$ npm set init.author.name "Daisuke Araki"
$ npm set init.author.email "公開可能なメールアドレス"

npmjsへのユーザ登録

$ npm adduser
Username: `登録ユーザ名`
Password: `パスワード`
Email: (this IS public) `登録メールアドレス`
Authenticator provided OTP: `ワンタイムパスワード`
Logged in as `username` on https://registry.npmjs.org/.

package.json設定

ここは色んなサイトに情報があるので割愛します。
リンクのみ置いておきます。

.npmignore作成

ここは任意です。各モジュールに合わせて必ず作成しましょう。
alexa-conversation-model-assertでは、以下を設定しています。

.npmignore
.editorconfig
.git
.gitignore
.istanbul.yml
coverage/
dist/test/
package-lock.json
src/
test/
tsconfig.json
tslint.json

公開

公開は以下のコマンドを実行するだけです。(カレントディレクトリ間違えないように!)

$ npm publish ./

+ [email protected]

まとめ

初めての公開なのでpublishコマンド実行前は、すごい不安がありプルプルしてましたが無事公開されました\(^o^)/
〜麻雀の符計算を Alexa にお願いするまで〜で投稿したじゃらじゃら 符計算スキルをv2へマイグレーションして使ってみたい!

ドキュメントやテストが不足しているので、まずはそこからブラッシュアップしていきます!
何も考えずとりあえず日本語で作っちゃったので、英語でも展開したいなぁと思います。

不具合や機能追加などご意見あれば、こちらへお願いします!
GitHub - Issues

npm
https://www.npmjs.com/package/alexa-conversation-model-assert

github
https://github.com/daisukeArk/alexa-conversation-model-assert