Lambdaのテストを書くときにいつも書き方を忘れるのでメモします
jestとaws-sdk-client-mockを使います
環境整備
npm install -D aws-sdk-client-mock jest babel-jest @babel/core @babel/preset-env jest-html-reporters
bable.config.js
module.exports = {
presets: [
[
"@babel/preset-env"
]
],
}
jest.config.js
module.exports = {
clearMocks: true,
collectCoverage: true,
coverageDirectory: "coverage",
coveragePathIgnorePatterns: ["/node_modules/"],
reporters: [
'jest-html-reporters'
],
testEnvironment: "node",
moduleFileExtensions: ["js", "mjs", "json", "node"],
testRegex: ".*test.js",
testPathIgnorePatterns: ["/node_modules/"],
transform: {
"^.+\\.m?js?$": "babel-jest",
},
setupFiles: ["<rootDir>/jest.setup.js"],
};
jest.setup.js
// 最初に呼び出したいスクリプトを設定
// 環境変数の設定とかお好みで
process.env.TABLE_NAME = "hoge";
process.env.BUCKET_NAME = "hoge"
process.env.AWS_ACCESS_KEY_ID = "dummy";
process.env.AWS_SECRET_ACCESS_KEY = "dummy";
process.env.AWS_REGION = "us-west-2";
DynamoDBからItemを取得するコード
import { DynamoDBDocumentClient, GetCommand } from "@aws-sdk/lib-dynamodb";
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
const initDynamoClient = () => {
const marshallOptions = {
removeUndefinedValues: true,
};
const translateConfig = { marshallOptions };
const DynamoDBclient = new DynamoDBClient({
region: 'ap-northeast-1'
});
const dynamo = DynamoDBDocumentClient.from( DynamoDBclient, translateConfig );
return dynamo;
}
const response = (statusCode, body) => {
return {
statusCode: statusCode,
headers: {
"Access-Control-Allow-Headers" : "Content-Type",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET"
},
body: JSON.stringify(
body
),
};
};
export const handler = async (event) => {
try {
const dynamo = initDynamoClient();
const id = event.pathParameters.id;
const dynamo_data = await dynamo.send(
new GetCommand({
TableName: "Dynamo_test",
Key: {
id: id,
},
})
);
if (dynamo_data.Item === undefined) {
return response(404, {"message": "not found."});
} else {
return response(200, dynamo_data.Item);
}
} catch (error) {
console.log(error);
return {
statusCode: 500,
headers: {
"Access-Control-Allow-Headers" : "Content-Type",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET"
},
body: JSON.stringify({"message": "500err"}),
};
}
};
テストコード
import { handler as getItem } from "../get_dynamo.mjs";
import { DynamoDBDocumentClient, GetCommand } from "@aws-sdk/lib-dynamodb";
import { mockClient } from "aws-sdk-client-mock";
const ddbMock = mockClient(DynamoDBDocumentClient)
describe("handler get item", () => {
beforeEach(() => {
ddbMock.reset()
})
it("get item test", async () => {
const event = {
"pathParameters": {
"id": "test"
}
};
const expectValue = {
Item: { id: "test", value: "memo" }
};
// すべてのGetCommandの結果を同じにしたい場合resolvesでOK
// 1回目の呼び出しと2回目のレスポンスを変更したい場合resolvesOnceを使う
ddbMock.on(GetCommand).resolves(expectValue)
const result = await getItem(event)
expect(result).toEqual({
statusCode: 200,
headers: {
"Access-Control-Allow-Headers" : "Content-Type",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET"
},
body: JSON.stringify(
{id:"test",value:"memo"}
)
})
const callsOfGet = ddbMock.commandCalls(GetCommand);
// コマンドが何回呼ばれたか検証
expect(callsOfGet.length).toBe(1);
// 指定されたパラメータで呼ばれたか検証
expect(callsOfGet[0].args[0].input).toEqual({
TableName: "Dynamo_test",
Key: {
"id": "test"
},
});
}
it("item not found test", async () => {
const event = {
"pathParameters": {
"id": "notfoundID"
}
};
const expectValue = {
Item: undefined
};
ddbMock.on(GetCommand).resolves(expectValue)
const result = await getItem(event)
expect(result).toEqual({
statusCode: 404,
headers: {
"Access-Control-Allow-Headers" : "Content-Type",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET"
},
body: JSON.stringify(
{"message": "not found."}
),
});
})
it("handler response test", async () => {
const event = {
"pathParameters": {
"id": "test"
}
};
const expectValue = {
Item: { id: event.pathParameters.id, value: "memo" }
};
ddbMock.on(GetCommand).resolves(expectValue)
const result = await getItem(event)
expect(result).toEqual({
statusCode: 200,
headers: {
"Access-Control-Allow-Headers" : "Content-Type",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET"
},
body: JSON.stringify(
expectValue.Item
),
});
})
})
describe("handler get item error test", () => {
beforeEach(() => {
ddbMock.reset()
})
it("handler error test", async () => {
const event = {
"pathParameters": {
"id": "test"
}
};
const expectValue = "ERR"
// errを返すときはrejects
ddbMock.on(GetCommand).rejects(expectValue)
const result = await getItem(event)
expect(result).toEqual({
statusCode: 500,
headers: {
"Access-Control-Allow-Headers" : "Content-Type",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET"
},
body: JSON.stringify(
{"message": "500err"}
),
});
})
})
おまけ
時間を固定で設定する
beforeEach(async () => {
const fixed = new Date("2024-01-01T00:00:00");
jest.useFakeTimers().setSystemTime(fixed.getTime());
});
モジュールを雑にモック
// fsモジュール全体をモック化
jest.mock("node:fs", () => ({
...jest.requireActual("node:fs"),
writeFileSync: jest.fn(),
existsSync: jest.fn(),
readFileSync: jest.fn(),
}));
テストコード内で返り値設定
// ディレクトリが存在すると仮定
fs.existsSync.mockImplementation(() => true);
fs.readFileSync.mockImplementation(() =>
JSON.stringify([
{
name: "検証",
Id: "00001",
}
])
);
fs.writeFileSync.mockImplementation(() => {});