echo("備忘録");

IT技術やプログラミング関連など、技術系の事を備忘録的にまとめています。

【Serverless Framework】Serverless Framework + TypeScript + JestでAWS Lambdaの開発&テスト環境を構築する その1

はじめに

皆さん、先日はVS Code Conferenceの視聴、ありがとうございました。
※見てない人は下のURLから視聴できます。

VS Code Conference Japan - YouTube

今年はVS Codeで登壇を結構させて頂きましたが、肝心の&僕が愛してやまないServerless Frameworkでの登壇が少ないなあ...なんて思ってます。

そういう理由...でもないですが、今回は久々にServerless Frameworkのブログです。

Serverless Framework公式ページ:
www.serverless.com

やること

タイトルの通り、今回は下記の環境でAWS Lambdaの開発環境を構築し、Jestでのテストも行えるようにします。

  • Serverless Framework
  • TypeScript
  • Jest

特に、今までずっとNode.jsでコードを書いてたので、TypeScriptもしっかりやらないと...という思いが強いです。(むしろ、本来は静的型付け言語の方が好きなので)

前提

とりあえず、上記3つのインストールを済ませておきます。(下記コマンドは、公式サイトのものをそのまま記載)

※global(-g) installではない場合、この後の「プロジェクト作成」の後で実行した方が良いです。(理由は後述。ただServerless Frameworkはどうしようもないけど...)

# Serverless Framework  
> npm install serverless -g  
  
# TypeScript  
> npm install typescript -g  
  
# Jest(もちろんglobal(-g) installでもOK)  
> npm install --save-dev jest  

で、Jestの設定をjest.config.jsやpackage.jsonに書きます。(下記はpackage.jsonに記載した場合)

{  
  "jest": {
    "roots": [
      "<rootDir>/"
    ],
    "testMatch": [
      "**/__tests__/**/*.+(ts|tsx|js)",
      "**/?(*.)+(spec|test).+(ts|tsx|js)"
    ],
  },
}

Serverless Frameworkプロジェクト作成

まずはServerless Frameworkのプロジェクトを作成します。
「--template」に「aws-nodejs-typescript」を指定すれば、TypeScriptのオプションを作成できます。

# 下記にはないけど、「--path」オプションでプロジェクトを作成する
# フォルダを指定できる。 (未指定時はカレントフォルダに作成)    
> serverless create --template aws-nodejs-typescript --name serverless-typescript-test

注意点としては、下記の点です。

  • 対象フォルダにすでに「package.json」や「tsconfig.json」があると、エラーになりますので、一時的にリネームor別フォルダに退避しておいてください。
    • 先程「グローバルではないnpm installはプロジェクト作成の後で」と書いたのはそのため
  • TypeScriptpプロジェクトの場合、テンプレートファイルが「serverless.yml」ではなく「serverless.ts」ファイルになります。
    • もちろん定義内容はまったく同じ。むしろ型定義ファイルによるインテリセンスが効くので、なかなか便利。

テスト対象のLambda関数の作成

まずはテスト対象のLambda関数の作成ということで、下記ソースを書きました。
※今回、コードの詳細には触れません。概要はソースコメントを参照。

// Day.jsのimport
import dayjs, { Dayjs } from 'dayjs'
import utc from 'dayjs/plugin/utc'
import timezone from 'dayjs/plugin/timezone'
  
dayjs.extend(utc)
dayjs.extend(timezone)
  
// Lambdaだったり、DynamoDBのimport
import { APIGatewayProxyEvent, APIGatewayProxyHandler, APIGatewayProxyResult } from 'aws-lambda';
import 'source-map-support/register';
import { Context } from 'vm';
import * as AWS from 'aws-sdk';
import { DocumentClient } from 'aws-sdk/clients/dynamodb';
  
// ハンドラ関数
export async function hello(event:APIGatewayProxyEvent, _context:Context):Promise<APIGatewayProxyResult>{
  console.log(`[event] ${JSON.stringify(event)}`);
  let response: APIGatewayProxyResult = null;
  
  const items:DocumentClient.ItemList = await getDynamoData();
  const dateTime:string = getDate();
  const nodeVer: string = getNodeJsVersion();
  
  response = {
    statusCode: 200,
    body: JSON.stringify({
      items: items,
      dateTime: dateTime
    })
  };
  
  return response;
}
  
// DynamoDBからquery()関数で特定データを抽出する
export async function getDynamoData():Promise<DocumentClient.ItemList> {
  
  const documentClient: DocumentClient = new AWS.DynamoDB.DocumentClient();
  const param: DocumentClient.QueryInput = {
    TableName: 'nature-remo-events-history',
    KeyConditionExpression: 'app_name = :app_name and date_time_num = :date_time_num',
    ExpressionAttributeValues: {
      ':app_name': 'NatureRemoTest',
      ':date_time_num': 20201122073504
    }
  };
  
  const data: DocumentClient.QueryOutput = await documentClient.query(param).promise();
  console.info(`[data] ${JSON.stringify(data)}`);
  
  return data.Items;
}
  
// 現在の日付or指定した日時の日付をISO形式で返す
export function getDate(dateTime?:string | number, format?:string): string {
  const moment:Dayjs = (function():Dayjs {
    const momentInterim:Dayjs = dateTime ? dayjs(dateTime) : dayjs();
    return momentInterim.tz('Asia/Tokyo');
  })();
  
  const momentString:string = format ? moment.format(format) : moment.format();
  return momentString;
}

今回、「Day.js」という日付操作ライブラリを使ってます。
機能開発停止&新プロジェクトへの採用を非推奨としたmoment.jsが、その代替として推奨しているモジュールの一つです。
実際、関数なども大部分がmoment.jsと同じで、扱いやすいです。

day.js.org

上記ソースをデプロイして、無事にデプロイ&正常動作を確認できればOKです。

※デプロイ実行前に「npm install」コマンドでnpmモジュールをインストールしないとエラーになりますので、そこは注意です。(プロジェクト作成時にpackage.jsonに必要なモジュールを記載してくれるが、インストールはしてくれない)

テストを書く

では、上記Lambdaのテストを書きましょう。
とりあえずは、getDate()関数のテストを作成します。(ソースは下記)

※なお、jestの型定義ファイルをインストールしておくと、テストコードの作成時に便利です。

# jestの型定義ファイルのインストール
> npm install -D @types/node
// getDate()関数のテストソース  
import {getDate} from './handler';
  
describe('hello.tsのテスト', () => {
  
    describe('getDate()のテスト', () => {
        test('ISO形式の文字列で時間指定した場合、正しくその時間が返ること', () => {
            expect(getDate('2020-11-28T09:00:00+09:00')).toBe('2020-11-28T09:00:00+09:00');
        });
  
        test('UNIX数値(ミリ秒)で時間指定した場合も、正しくその時間が返ること', () => {
            expect(getDate(1606521600000)).toBe('2020-11-28T09:00:00+09:00');
        });
    })
});

で、上記ソースを「jest」コマンドで実行して、問題なければOKです。

が、下記エラーが発生するケースがあります。(私もそうだった)

Jest encountered an unexpected token.
This usually means that you are trying to import a file which Jest cannot parse, e.g. it's not plain JavaScript.
By default, if Jest sees a Babel config, it will use that to transform your files, ignoring "node_modules".

原因ですが、上のエラーにもある通り、「プレーンなJavaScriptに変換できないコードがある」のが原因です。
(テストソース的には「import {getDate} from './handler'」が該当)

というか、Jestの公式ドキュメントにも

Jest supports TypeScript, via Babel. First, make sure you followed the instructions on using Babel above. Next, install the @babel/preset-typescript via yarn

とある通り、JestでTypeScriptをテストする場合、Babelと「@babel/preset-typescript」モジュールを入れる必要があります。

なので、これらをインストールします。

Babel&@babel/preset-typescriptのインストール

といっても、ここからはJestの公式ドキュメントの記載とほとんど同じです。

まずはBabelを下記コマンドでインストールして、

# babel-jestはjestインストール時にインストールされるので、  
# ない場合のみインストールする。
> npm i -D babel-jest @babel/core @babel/preset-env

で、babel.confog.jsなりpackage.jsonに下記設定を追記します。(下記はpackage.jsonの場合)

"babel": {
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "node": "current"
        }
      }
    ],
  ]
}

で、次に「@babel/preset-typescript」モジュールをインストールします。

> npm i -D @babel/preset-typescript

で、同じようにbabel.confog.jsなりpackage.jsonに設定を追記します。(下記はpackage.jsonの場合)
※なお、Jest公式ページでは「@babel/preset-typescript」を配列にしていませんが、配列にしないと正しく動かないので注意。

"babel": {
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "node": "current"
        }
      }
    ],
    [
      "@babel/preset-typescript" 
    ]
  ]
}

また「babel-jestはbabelの設定がある場合、自動でファイル変換を行う」とのことですが、自分でJestの「transform」に設定を定義することで、任意のtransform設定を定義出来ます。

{  
  "jest": {
    "roots": [
      "<rootDir>/"
    ],
    "testMatch": [
      "**/__tests__/**/*.+(ts|tsx|js)",
      "**/?(*.)+(spec|test).+(ts|tsx|js)"
    ],
    "transform": {
      "^.+\\.(ts|tsx)$": "babel-jest"
    },
  },
}

動作確認

では、改めて動作確認です。

上記設定をした後で「jest」コマンドを動かすと...

f:id:Makky12:20201203075406p:plain

正しくテストが実行されました。これでOKです。(skipしたテストは、その2で取り上げる予定です)

まとめ

これで、Serverless Framework + TypeScript + JestでのAWS Lambda開発環境が整いました。

TypeScriptは個人的に使いやすいと感じますし、型厳格な言語(C#とかJavaとか)が好きな人(自分も含め)にお薦めなので、これからもどんどん使っていきたいと思っています。

なお「てか、Serverless Frameworkほとんど関係ねえじゃん!」と思った人もいるかもしれませんが、その2でServerless Frameworkのプラグインを使ったテストを紹介する予定です。

告知

現在Qiitaで開催されている、「Advent Calendar2020」に「Serverless」と「AWS」で記事を公開します。
Serverlessは12/18(金)、AWSは12/25(金)に公開予定ですので、よろしければそちらもどうぞ。

なお、Serverlessは「Serverless Framework はじめの一歩」、AWSは「aws-sdk-mockを使ったAWSのテスト」について書く予定です。

qiita.com

qiita.com

それでは、今回はこの辺で。