Serverless Framework + TypeScript（esbuild）を JestでUnitテストしてみる

前記事（[**Serverless Framework + TypeScript（esbuild）で ローカル開発環境構築と、AWS（API Gateway + Lambda + DynamoDB）へのデプロイをしてみる**](https://qiita.com/chinone0711/items/abea3d31a2780297b1da)）で作ったModelプログラムをJestを使ってUnitテストしてみます。



# 事前準備
プロジェクトルートのディレクトリに移動します。

### Jestをグローバルにインストール
```shell
npm i -g jest
```

### 関連するライブラリをインストール
```shell
npm i -D jest @types/jest
npm i -D esbuild-jest
```

# Jestの設定ファイルを作成
```JavaScript: jest.config.js
module.exports = {
  testEnvironment: 'node',        // テスト環境
  transform: {
    "^.+\\.tsx?$": "esbuild-jest" // transformerにesbuild-jest
  },
  globalSetup: "./setupEnv.ts",   // テスト用に環境変数ファイルを取得
};
```

# テスト用の環境変数ファイルを作成
localで実行するようにします。
```TypeSCript: setupEnv.ts
export default (): void => {
    console.log("\nSetup test environment");
    process.env.ENV = "local";
    process.env.TZ = "Asia/Tokyo";
    console.log("process.env.ENV", process.env.ENV);
    console.log("process.env.TZ", process.env.TZ);
    return;
};
```

# src/model/User.tsファイルを編集
```TypeScript: src/model/User.ts（修正前）
　　：
　　：
    constructor() {
        if (global.dynamodbClient) {
            this.client = global.dynamodbClient;
        } else {
            this.client = new DynamoDBClient({
                region: process.env.AWS_REGION,
            });
        }
        this.documentClient = DynamoDBDocumentClient.from(this.client);
    }
　　：
　　：
```
　　　　↓　↓　↓　↓　↓　↓
```TypeScript: src/model/User.ts（修正後）
　　：
　　：
    constructor() {
        if (global.dynamodbClient) {
            this.client = global.dynamodbClient;
        } else {
            if (process.env.ENV === "local") {
                this.client = new DynamoDBClient({
                    region: process.env.AWS_REGION,
                    endpoint: "http://localhost:8000", // <===== これを指定しないと、DynamoDBアクセス時、AWS profile(./aws/credentials)のデフォルト設定にアクセスしてしまうので注意！
                });
            } else {
                this.client = new DynamoDBClient({
                    region: process.env.AWS_REGION,
                });
            }
        }
        this.documentClient = DynamoDBDocumentClient.from(this.client);
    }
　　：
　　：
```
しばらくの間「**endpoint: "http\://localhost:8000"** 」をつけずにJest実行していて、リモートのAWSのDynamoDBにアクセスしていたので、ここで数時間ハマりました。

# src/model/User.tsファイルにテストで使う関数を追加
何に使うかと言うと、ユーザーデータを更新したあとに、確かに更新されたかどうかDBからデータを取得する為に利用します。
```TypeScript: src/model/User.ts
　　：
　　：
    /**
     * IDからユーザーを取得
     * @param id
     * @returns
     */
    getById = async (id: string) => {
        let ret;
        const params: QueryCommandInput = {
            TableName: this.userTableName,
            KeyConditionExpression: "#key = :value",
            ExpressionAttributeNames: {
                "#key": "id",
            },
            ExpressionAttributeValues: {
                ":value": { S: id },
            },
        };

        try {
            const command = new QueryCommand(params);
            const data = await this.client.send(command);
            if (data && data.Items) {
                for (const item of data.Items) {
                    ret = item;
                    break;
                }
            }
        } catch (err) {
            console.error("Unable to query. Error users(1.1):", JSON.stringify(err, null, 2));
            throw new Error(`Unable to query. Error users(1.1): ${err.message}`);
        }
        return ret;
    };
　　：
　　：
```

# Unitテストを書いてみる（ここが本題）
```shell
mkdir test # テストスクリプト用ディレクトリを作成
cd test
```

ローカルのDynamoDBに対して、以下を順番に実施します。
1. User登録に関するテスト
1. User取得に関するテスト
1. User更新に関するテスト
1. User削除に関するテスト

### ソースコードの補足説明
**describe** : テストのグルーピングに使います。
**it** : testと同じ意味で、テスト関数になります。


```TypeScript: test/User.test.js
import { User } from "../src/model/User"; // テストするModelを読み込みます
import { setTimeout } from "node:timers/promises"; // スリープ用のsetTimeoutを読み込みます

const USER_SIGNATURE = "test1234"; // 今回のSignature（本番データと被らなければなんでも良いです）

describe("User登録に関するテスト", () => {
    it("新規登録のテスト", async () => {
        const user = new User();

        // ユーザー作成
        const createdUserId = await user.createUser(USER_SIGNATURE, "山田", "男性");

        // SignatureからユーザーIDを取得
        const getUserId = await user.getIdBySignature(USER_SIGNATURE);

        // 作成したユーザーが存在するかチェック
        expect(createdUserId).toBe(getUserId);
    });

    it("二重登録エラーのテスト", async () => {
        const user = new User();

        // わざと同一のSignatureでユーザーを登録してエラーが出るかチェック
        try {
            const result = await user.createUser(USER_SIGNATURE, "山田", "男性");
        } catch (e) {
            // 許容するエラーメッセージ（Transaction cancelled ～）かチェック
            expect(e.message).toContain("Unable to put item. Error users(2): Transaction cancelled");
        }
    });
});

describe("User取得に関するテスト", () => {
    it("一覧取得テスト", async () => {
        const user = new User();

        // ユーザー一覧を取得
        const users = await user.getAll();

        // ユーザー一覧が存在する（0件超か）かチェック
        expect(users.length).toBeGreaterThan(0);
    });
});

describe("User更新に関するテスト", () => {
    it("更新テスト", async () => {
        const user = new User();

        // SignatureからユーザーIDを取得
        const getUserId = await user.getIdBySignature(USER_SIGNATURE);

        // 更新後のユーザー情報
        const req = {
            userId: getUserId,
            body: {
                user: {
                    name: "鈴木",
                    gender: "女性",
                },
            },
        };

        // ユーザー情報の更新
        await user.updateUserById(req);

        // データ更新されるまで1秒待つ（ローカルのDynamoDBが更新に時間かかるので待つようにしました）
        await setTimeout(1000);

        // 更新されたユーザー情報をDBから取得
        const updatedUser = await user.getById(getUserId);

        // 正しく更新されたかチェック
        expect(updatedUser.name.S).toBe("鈴木");
        expect(updatedUser.gender.S).toBe("女性");
    });
});

describe("User削除に関するテスト", () => {
    it("削除テスト", async () => {
        const user = new User();

        // SignatureからユーザーIDを取得
        const getUserId = await user.getIdBySignature(USER_SIGNATURE);

        // 削除するユーザー情報
        const req = {
            headers: {
                signature: USER_SIGNATURE,
            },
            userId: getUserId,
        };

        // ユーザー情報の削除
        await user.deleteUserById(req);

        // データ削除されるまで1秒待つ（ローカルのDynamoDBが削除に時間かかるので待つようにしました）
        await setTimeout(1000);

        // 正しく削除されたかチェック（ユーザー情報が取れていない（空白）ならOK）
        const checkUserId = await user.getIdBySignature(USER_SIGNATURE);
        expect(checkUserId).toBe("");
    });
});

```

# テストを実行してみる
```shell
$ jest  # 入力してEnter
Determining test suites to run...
Setup test environment
process.env.ENV local
process.env.TZ Asia/Tokyo
  console.error  # <===== User.tsの中で、エラーをコンソールで出すようにしているので表示されます。
    Unable to put item. Error users(2): {
      "name": "TransactionCanceledException",
      "$fault": "client",
      "$metadata": {
        "httpStatusCode": 400,
        "requestId": "4bf3f3bf-8542-4d2d-a353-2d1dd77d5bdb",
        "attempts": 1,
        "totalRetryDelay": 0
      },
      "CancellationReasons": [
        {
          "Code": "None"
        },
        {
          "Code": "ConditionalCheckFailed",
          "Message": "The conditional request failed"
        }
      ],
      "__type": "com.amazonaws.dynamodb.v20120810#TransactionCanceledException",
      "message": "Transaction cancelled, please refer cancellation reasons for specific reasons [None, ConditionalCheckFailed]"
    }

      146 |         } catch (err) {
      147 |             console.error("Unable to put item. Error users(2):", JSON.stringify(err, null, 2));
    > 148 |             throw new Error(`Unable to put item. Error users(2): ${err.message}`);
          |                 ^
      149 |         }
      150 |         return ret;
      151 |     };

      at User.createUser (src/model/User.ts:148:17)
      at Object.<anonymous> (test/User.test.ts:14:22)

 PASS  test/User.test.ts # <===== テストは正常に通過
  User登録に関するテスト
    ✓ 新規登録のテスト (41 ms)
    ✓ 二重登録エラーのテスト (18 ms)
  User取得に関するテスト
    ✓ 一覧取得テスト (6 ms)
  User更新に関するテスト
    ✓ 更新テスト (1011 ms)
  User削除に関するテスト
    ✓ 削除テスト (1013 ms)

Test Suites: 1 passed, 1 total
Tests:       5 passed, 5 total
Snapshots:   0 total
Time:        2.381 s, estimated 3 s
Ran all test suites.
```
ローカルのDynamoDBのデータを作成して、更新、削除しているので、データ上は分からないですが、コンソール上では正常に動作していますね。

# ソースを監視し、変更があると自動でテストを実行
以下コマンドを実行します。
すると、プログラム変更があったら自動でテストしてくれます。
```shell
jest --watchAll
```

以上