はじめに
皆さん、先日は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と同じで、扱いやすいです。
上記ソースをデプロイして、無事にデプロイ&正常動作を確認できれば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」コマンドを動かすと...

正しくテストが実行されました。これで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のテスト」について書く予定です。
それでは、今回はこの辺で。