はじめに
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
$ touch docker-compose.yml
githubの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
の一番最後の行ですね。
では、起動してみましょう。
$ 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ファイルを作成し、ニューステーブルへロードします。
$ touch 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文の下に以下を追記します。
// ~~~省略~~~
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をつつくようにする
になります。
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
this.repository = (repository) ? repository : new NewsRepository(new AWS.DynamoDB());
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
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を使って単体テスト