【aws-sdk-client-mockでDynamoDBモックを作成!】Lambda with TypeScriptをjestでテストする
この記事で解決すること
- TypeScriptで書かれた[DynamoDB操作を行うLambda関数]のテストコードを書く流れ
- テストのためのDynamoDBテーブル・データを用意する方法
前提の補足
例えばsam init
を用いてプロジェクトを初期化した際にTypeScript使用を指定するとあらかじめjestを実行できるようにセットアップされています。
今回はそういったセットアップがない状態、0からのテスト環境構築を想定しています。
環境
- Node.js v20
- AWS SDK for JavaScript v3
ディレクトリ構成
├── jest.config.js
├── package-lock.json
├── package.json
├── src
│ ├── app.ts
├── tests
│ ├── app.test.ts
│ └── events
│ └── scan-event.ts
└── tsconfig.json
アジェンダっぽいもの
まず結論
jest / ts-jest / aws-sdk-client-mock を使用することで簡単にテストを実行できる
話す流れ
- 必要なパッケージおよび設定ファイルについて
- テスト対象のコードを見る
- テストコードを見る
- テストコードの解説
- テストコードを書く流れ
話さないこと・考慮していないこと
テストコードのベストプラクティス
必要なパッケージのインストール
こちらのドキュメントを参考に進めます。
npm install --save-dev jest
TypeScriptを使用する場合、Babelもしくはts-jestを経由する必要があります。
Babelを使う
npm install --save-dev babel-jest @babel/core @babel/preset-env
npm install --save-dev @babel/preset-typescript
babel-config.js
)を書く
設定ファイル(module.exports = {
presets: [
['@babel/preset-env', {targets: {node: 'current'}}],
'@babel/preset-typescript',],
};
ts-jestを使う
npm install --save-dev ts-jest
@jest/globals
または@types/jest
をインストールしてテストコードをTypeScriptで書く
jestで使われる型定義されたAPIを使用するために@jest/globals
をインストールします。
npm install --save-dev @jest/globals
こちらをインストールしないと、jestのための記述で型エラーが発生。
【例:以下のdescribe()
など】
import {describe, expect, test} from '@jest/globals';
import {sum} from './sum';
describe('sum module', () => {
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
});
[参考]:https://jestjs.io/ja/docs/getting-started#type-definitions
または、@types/jest
をインストールするとインポートすることなく型が解決されます。
npm install --save-dev @types/jest
// describe等のimport文は不要になる
import {sum} from './sum';
describe('sum module', () => {
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
});
Babel VS ts-jest
Babelはあくまでコンパイラとしての役割です。Jestもまたテストコードの型チェックを行わないません。
型チェックを行いたい場合はts-jestを使用します。(もしくはTypeScriptコンパイラであるtsc
を別途で使用する。
[参考]:https://jestjs.io/ja/docs/getting-started#babel-経由で
jest.config.js
)
設定ファイル(最低限、以下の記述をします。
module.exports = {
"roots": [
"<rootDir>/tests/",
],
"transform": {
"^.+\\.ts": ["ts-jest", {
tsconfig: "tsconfig.json",
}],
},
"testMatch": [
"<rootDir>/tests/**/*.test.ts",
],
}
roots
jestがテストファイルを検索するルートディレクトリを指定。
transform
ファイルの変換方法を指定。
^.+\\.ts
は.ts
で終わる全てのファイルをマッチさせ、ts-jest
によりtsconfig.json
ファイルを用いてTypeScriptコンパイルするという方法を指定しています。
testMatch
どのファイルをテストファイルとして扱うか指定。
package.jsonにscriptsを追加
npm run test
で実行できるようにpackage.jsonも記述します。
"scripts": {
"test": "jest"
},
テストコードを書く準備
ここまではjestの設定でした。
早速テストコードを書いてみます。
※今回はts-jestを用いて進めていきます
テスト対象のコード
DynamoDBのテーブルからデータをscanしてそのまま返すlambda関数を例として作成します。
import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
import { DynamoDBClient, DynamoDBClientConfig } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, ScanCommand, ScanCommandInput, ScanCommandOutput } from "@aws-sdk/lib-dynamodb";
const options: DynamoDBClientConfig = {
endpoint: process.env.DYNAMODB_ENDPOINT
}
const docClient = DynamoDBDocumentClient.from(new DynamoDBClient(options));
export const handler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
const params: ScanCommandInput = {
TableName : process.env.TABLE_NAME
};
const command = new ScanCommand(params);
const result: ScanCommandOutput = await docClient.send(command);
return {
statusCode: 200,
body: JSON.stringify(result.Items),
};
}
ここで私が疑問に思ったことと答え(DynamoDBのモック)
DynamoDB処理を行う関数をテストしたいが、テストのためのデータをどのように用意するのだろう?どこにどうテーブルを用意するのだろう?モックの使い方は?と思いました。
Lambda関数でDynamoDB操作を行うためにAWS SDKを使用します。SDKを用いた操作をテストするために、aws-sdk-client-mock
というパッケージがあります。
インストールする
npm install --save-dev aws-sdk-client-mock
参考
aws-sdk-client-mock
はAWS SDK For JavaScript v3のためのオープンソースライブラリです。
AWS公式ブログで紹介されていました!
jestとaws-sdk-client-mockを用いてテストコードを書く
まずはテストコード
import { mockClient } from 'aws-sdk-client-mock';
import { DynamoDBDocumentClient, ScanCommand } from '@aws-sdk/lib-dynamodb';
import { scanEvent } from './events/scan-event';
import { handler } from '../src/app';
import { APIGatewayProxyResult } from 'aws-lambda';
const ddbMock = mockClient(DynamoDBDocumentClient);
const items = JSON.parse(`{
"Items": [
{
"id": "1",
"name": "John",
"age": 30
},
{
"id": "2",
"name": "Jane",
"age": 25
}
]
}`);
beforeAll(() => {
process.env.TABLE_NAME = 'hoge-table';
});
beforeEach(() => {
ddbMock.reset();
});
describe('handlerのテスト', () => {
it('hoge tableをscanする', async () => {
ddbMock.on(ScanCommand, { TableName: 'hoge-table' }).resolves({ Items: items });
const response = await handler(scanEvent) as APIGatewayProxyResult;
expect(response.statusCode).toBe(200);
expect(JSON.parse(response.body)).toEqual(items);
});
});
解説
aws-sdk-client-mock
の mockClient を用いてモックを初期化する
この一行でモック構築完了!
const ddbMock = mockClient(DynamoDBDocumentClient);
items
にモックデータを定義
テストで使用するデータを定義します。
items
const items = JSON.parse(`{
"Items": [
{
"id": "1",
"name": "John",
"age": 30
},
{
"id": "2",
"name": "Jane",
"age": 25
}
]
}`);
テスト前の処理
beforeAll()
は全てのテストの前に一度だけ実行されます。
ここでは、テスト対象のコード内で使用している環境変数設定のために使用。
beforeAll
beforeAll(() => {
process.env.TABLE_NAME = 'hoge-table';
});
beforeEach()
は各テスト前に実行されます。モックをリセット。
beforeEach
beforeEach(() => {
ddbMock.reset();
});
テストスイート定義
describe()
でテストスイートを定義。グループ化しています。
it()
で各テストケースを定義。
describe('handlerのテスト', () => {
it('hoge tableをscanする', async () => {
ddbMock.on(ScanCommand, { TableName: 'hoge-table' }).resolves({ Items: items });
const response = await handler(scanEvent) as APIGatewayProxyResult;
expect(response.statusCode).toBe(200)
expect(JSON.parse(response.body)).toEqual(items);
});
});
一行ずつ見ていきましょう。
モック設定
ddbMock.on(ScanCommand, { TableName: 'hoge-table' }).resolves({ Items: items });
ここではまずモックを設定しています。ScanCommandがhoge-tableに対して実行された時に、上記で定義したitems
が返されるように設定。
関数の実行
const response = await handler(scanEvent) as APIGatewayProxyResult;
テスト対象コードのhandler関数を呼び出し、引数scanEvent
には実際に渡すイベントを指定。
今回は別ファイルで定義したjsonデータを使用しています。(ローカル実行時のログを参考に作成)
event.jsonの定義
import { APIGatewayProxyEvent } from "aws-lambda"
export const scanEvent: APIGatewayProxyEvent = {
"body": null,
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate, br",
"Connection": "keep-alive",
"Host": "127.0.0.1:3000",
"User-Agent": "",
"X-Forwarded-Port": "3000",
"X-Forwarded-Proto": "http"
},
"httpMethod": "GET",
"isBase64Encoded": false,
"multiValueHeaders": {
"Accept": ["*/*"],
"Accept-Encoding": ["gzip, deflate, br"],
"Connection": ["keep-alive"],
"Host": ["127.0.0.1:3000"],
"X-Forwarded-Port": ["3000"],
"X-Forwarded-Proto": ["http"]
},
"multiValueQueryStringParameters": null,
"pathParameters": null,
"queryStringParameters": null,
"requestContext": {
"accountId": "123456789012",
"apiId": "1234567890",
"domainName": "127.0.0.1:3000",
"extendedRequestId": null,
"httpMethod": "GET",
"identity": {
"accountId": null,
"apiKey": null,
"caller": null,
"cognitoAuthenticationProvider": null,
"cognitoAuthenticationType": null,
"cognitoIdentityPoolId": null,
"sourceIp": "127.0.0.1",
"user": null,
"userAgent": "Custom User Agent String",
"userArn": null
},
"protocol": "HTTP/1.1",
"requestId": "64a55bae-4d11-43f9-96f4-e21ae01b6b2d",
"resourceId": "123456",
"resourcePath": "/hoge",
"stage": "Prod"
},
"resource": "/hoge",
"stageVariables": null
} as any
テスト結果の検証
expect(response.statusCode).toBe(200)
expect(JSON.parse(response.body)).toEqual(items);
expect()
で結果を検証します。
テストの実行結果
$ npm run test
> test
> jest
PASS tests/app.test.ts
handlerのテスト
✓ hoge tableをscanする (2 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.025 s
Ran all test suites.
テストコードの流れをまとめてみる
テストのためにDynamoDBテーブルおよびデータを用意することなく、aws-sdk-client-mock
を使用してDynamoDB操作を行うLambda関数のテストができました。
構築したモッククライアントに対して対象の操作(ScanCommand
)が行われた時の結果を設定。
テスト対象の関数を呼び出し・実行。この関数内ではScanCommandを実行しており、モックに設定した結果が返却されます。
- DynamoDB操作時の挙動をモックに対して設定
- 関数を実行
- 結果の検証
という流れです。
今回は簡単なScan操作のみテストだったのでモック設定のためのデータ=期待されるデータ(items
)となりましたが、実際にさらに複雑な関数をテストする場合は別となるケースも多々あると考えられます。
- モック設定のためのデータ
- 期待されるデータ
私はテストコードの書き方を調査している際は理解に苦しみましたが、この二つを切り離して考えることで他のテストコードにも展開できるよう流れを理解することができました。
まとめ
サーバーレス構成でのテストコードを初めて書きました。(まだまだテストコード自体書き慣れていませんが・・・)
AWS上はもちろんローカル (※dynamodb-local)にデータを用意することも難しく、AWSリソースへの操作をどうテストするのだろう?と悩みましたが、aws-sdk-client-mock
で簡単にモック構築ができてとても便利でした。
モックに設定するデータやイベントデータの用意さえできればテスト用サーバーやコンテナなどを準備する必要がなく、サーバーレス開発らしく手軽にテストも実行できます。
本格的にテストコードを書いていくにあたって懸念点やベストプラクティスは何か?といった問題も出てくるので、さらに学んでいきたいです。
Discussion