LoginSignup
4
2

More than 5 years have passed since last update.

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

Posted at

はじめに

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を使って単体テスト

参考

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2