Posted at

TypeScript を使って Alexa Custom Skills を作ろう 番外編 localstack

More than 1 year has passed since last update.


はじめに

TypeScript を使って Alexa Custom Skills を作ろうで作成したスキルでは、ユーザが発話した内容に基いて何がAlexaから返されるかは、すべてAWS Lambda内の処理で完結しています。

しかし、実際にスキルを作り始めると以下のような要件が出てきます。

例えば、


  • 既存システムのDynamoDBにあるデータを取得したい

  • セッション情報を永続化したい

本投稿では、実装 - 発話編で作成したニュースリポジトリクラスを変更し、DynamoDBからデータを取得するようにします。


テストについての課題

本投稿で、ニュースリポジトリクラスをDynamoDBからデータを取得するように変更した場合、当然ですがDynamoDBにテーブルを作成しデータを登録する必要があります。

Amazon DynamoDBにそれらを登録をしアクセスすることにしてしまうとインターネット接続が必要となる為、気軽にローカルでテストをするということが難しくなってしまいます。


気軽にテストする為に考える

気軽にテストする為には、DynamoDBがローカル環境で実行されている必要があります。

その為の手段としてあるのが、


  • DynamoDB Local

  • localstack

のどちらかを利用することです。

本投稿では、リポジトリの修正と合わせてlocalstackを利用したテストの方法について、ご紹介します。


やってみよう


事前準備

localstack環境構築の為には、事前に以下のインストールが必要です。

本投稿では、インストール方法については割愛いたします。


  • docker

  • docker-compose

  • aws-cli


localstack環境の構築

まずは、localstack環境を作ります。

localstack/localstack - github


docker-compose.ymlの作成

Docker Composeを利用しコンテナの管理を行う為、設定ファイルを準備します。


移動

$ cd ~/custom-skill-sample-to-convert/skill



docker-compose.ymlの作成

$ touch docker-compose.yml


githubのdocker-compose.ymlを参考に以下の通り設定します。


docker-compose.yml

version: '2.1'

services:
localstack:
container_name: typescript-localstack
image: localstack/localstack
ports:
- "4567-4583:4567-4583"
- "${PORT_WEB_UI-8080}:${PORT_WEB_UI-8080}"
environment:
- DATA_DIR=/tmp/localstack/data
- LAMBDA_EXECUTOR=docker
volumes:
- "${TMPDIR:-/tmp/localstack}:/tmp/localstack"
- "/var/run/docker.sock:/var/run/docker.sock"
- "./__localstack__/data:/tmp/localstack/data:rw"


localstackでは、以下のサービスのデータを永続化することが可能です。


  • DynamoDB

  • Elasticsearch

  • Kinesis

  • S3

永続化するにあたり、ローカルディレクトリをマウントします。

volumesの一番最後の行ですね。

では、起動してみましょう。


localstack起動

$ TMPDIR=/private$TMPDIR docker-compose up

...
typescript-localstack | Starting mock ES service (http port 4578)...
typescript-localstack | Starting mock Lambda service (http port 4574)...
typescript-localstack | Ready.



AWS CLI プロファイル追加

localstack DynamoDBへはAWS CLIで操作を行います。

localstack用のプロファイルを追加します。


プロファイル追加

$ aws configure --profile localstack

AWS Access Key ID [None]: dummy
AWS Secret Access Key [None]: dummy
Default region name [None]: us-east-1
Default output format [None]: text

これでAWS CLIでlocalstackを操作する準備が出来ました。


DynamoDB テーブル作成とデータ登録

では、早速作成していきましょう。

ここでは、以下のコマンドを利用します。


  • create-table

  • batch-write-item


ニューステーブル作成

aws dynamodb create-table \

--profile localstack \
--endpoint-url=http://localhost:4569 \
--table-name news \
--attribute-definitions AttributeName=sequenceId,AttributeType=N \
--key-schema AttributeName=sequenceId,KeyType=HASH \
--provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1

データ登録は、jsonファイルを作成し、ニューステーブルへロードします。


ニュースデータjsonファイル作成

$ touch news.json


コンテンツを少し変えています。


news.json

{

"news": [
{
"PutRequest": {
"Item": {
"sequenceId": {
"N": "1"
},
"contents": {
"S": "1番ですね"
}
}
}
},
{
"PutRequest": {
"Item": {
"sequenceId": {
"N": "2"
},
"contents": {
"S": "2番ですね"
}
}
}
},
{
"PutRequest": {
"Item": {
"sequenceId": {
"N": "3"
},
"contents": {
"S": "3番ですね"
}
}
}
}
]
}


データロード

aws dynamodb batch-write-item \

--profile localstack \
--endpoint-url=http://localhost:4569 \
--request-items file://news.json

準備はこれで完了です。


ニュースリポジトリクラスの修正

では、ニュースリポジトリクラスを修正しDynamoDBからデータを取得するようにしましょう。


AWS Configの設定

localstackのDynamoDBに接続する為にプロファイルに合わせて、configを設定します。

index.tsのimport文の下に以下を追記します。


index.ts

// ~~~省略~~~

import { languageStrings } from './utterances/language-strings';

if (!process.env.NODE_ENV) {
// NODE_ENVが未定義の場合

// AWSコンフィグ設定
AWS.config.update({
accessKeyId: 'dummy',
secretAccessKey: 'dummy',
dynamodb: {
region: 'us-east-1',
endpoint: 'http://localhost:4569'
}
});
}



ニュースリポジトリクラスの修正

ニュースリポジトリクラスがDynamoDBへ接続するために必要な変更点は、


  • AWS SDKをインポートする

  • DynamoDBコンテキストへの依存解決する


  • getAsyncメソッドでDynamoDBをつつくようにする

になります。


news-repository.ts

import * as AWS from 'aws-sdk';

/**
* ニュースリポジトリクラス
*/

export class NewsRepository {
/**
* DBコンテキスト
*/

private dbContext: AWS.DynamoDB;

/**
* コンストラクタ
*/

constructor(dbContext: AWS.DynamoDB) {
this.dbContext = dbContext;
}

/**
* 内容取得
* @param sayNumber 数字
* @returns 内容
*/

public getAsync(sayNumber: string): Promise<string> {
return new Promise(async (resolve, reject) => {
// getItemパラメータ設定
const params: AWS.DynamoDB.GetItemInput = {
TableName: 'news',
Key: {
sequenceId: {
N: sayNumber
}
}
};

let result: AWS.DynamoDB.GetItemOutput;

try {
// データ取得
result = await this.dbContext.getItem(params).promise();
} catch (err) {
reject(err);
return;
}

// アイテムが未定義であるか判定
if (!result.Item) {
// 未定義の場合リジェクト
reject(`${sayNumber}番のデータは見つかりませんでした。`);
return;
}

// 結果を文字列で返す
resolve(String(result.Item.contents.S));
});
}
}



ニュースリポジトリクラス呼出元の修正

インスタンス作成時にDBコンテキストを受け取るように変更したため、合わせて修正を行います。


  • src/utterances/first-utterance.ts

  • src/tests/utterances/first-utterance.test.ts


src/utterances/first-utterance.ts(37行目)

this.repository = (repository) ? repository : new NewsRepository(new AWS.DynamoDB());



src/tests/utterances/first-utterance.test.ts(34行目)

const repository = new NewsRepository(new AWS.DynamoDB());



ニュースリポジトリクラス単体テスト

実際にDynamoDBから値が取得できるかを評価しましょう。


移動

$ cd ~/custom-skill-sample-to-convert/skill/lambda/custom



テストファイル作成

$ mkdir -p ./src/tests/models

$ touch ./src/tests/models/news-repository.test.ts



news-repository.test.ts

import * as AWS from 'aws-sdk';

import * as Chai from 'chai';
import * as Mocha from 'mocha';
import { NewsRepository } from '../../models/news-repository';

const assert = Chai.assert;
const should = Chai.should();

// AWSコンフィグ設定
AWS.config.update({
accessKeyId: 'dummy',
secretAccessKey: 'dummy',
dynamodb: {
region: 'us-east-1',
endpoint: 'http://localhost:4569'
}
});

/**
* テスト
*/

describe('news-repositoryクラスのテスト', () => {
// ターゲットインスタンス作成
const target = new NewsRepository(new AWS.DynamoDB());

/**
* テストケース
*/

describe('メッセージが正しく取得できること', () => {
it('インスタンスが作成されていること', () => {
should.not.equal(target, null);
should.not.equal(target, undefined);
});

it('想定した発話が返されること', async () => {
// 発話内容取得
const result = await target.getAsync('1');

// アサーション
assert.equal(result, '1番ですね');
});
});
});



トランスパイル

$ tsc



テスト実行&確認

$ $(npm bin)/mocha ./dist/tests/models/news-repository.test.js

news-repositoryクラスのテスト
メッセージが正しく取得できること
✓ インスタンスが作成されていること
✓ 想定した発話が返されること (72ms)

2 passing (79ms)


無事に2件パスしました。


まとめ

localstackを利用することで、AWSサービスとの連携が必要なスキル開発も気軽にローカルでテストが可能になりました。

今回はDynamoDBのみを利用しましたが、他サービスも利用することで出来ることの幅が広がりますね!

今回の投稿のソースは以下にあります。

localstackを使って単体テスト


参考