AWS JavaScript SDK v3 で jest を使う。aws-sdk-client-mock(-jest) の利用記録

# 背景
AWS 業務も一段落したので、AWS SDK for JavaScript V3 での jest の利用記録を残しておく 

# 方針
色々試してみて、最終的に以下とした
- [jest](https://jestjs.io/) で
    - TypeScript なら [ts-jest](https://github.com/kulshekhar/ts-jest) がよさげ
- [AWS JavaScript SDK v3](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/) には
    - [aws-sdk-client-mock](https://github.com/m-radzikowski/aws-sdk-client-mock) で Mock を
        - jest 連携は [aws-sdk-client-mock-jest](https://github.com/m-radzikowski/aws-sdk-client-mock#jest-matchers) を

# mock 概要
## client を以下の要領で Mock して
```typescript: mock 例
const snsMock = mockClient(SNSClient);
const userPoolMock = mockClient(CognitoIdentityProviderClient);
const dynamoDBdocMock = mockClient(DynamoDBDocumentClient);
const s3Mock = mockClient(S3Client);

const dynamoDB = new DynamoDBClient({});
const dynamoDBMock = mockClient(dynamoDB);

dynamoDBdocMock.onAnyCommand().resolves({}); // 全Command に対して定義
dynamoDBdocMock
    .on(GetCommand) // GetCommand 全部
    .resolves({
      Item: [],
    })
    .on(GetCommand, {  // GetCommand Property の部分一致
      IndexName: "index-Age",
    })
    .resolves({
      Item: [],
    });
```

## aws-sdk-client-mock-jest を使うと楽になる
```typescript: mtaches
import 'aws-sdk-client-mock-jest';

expect(snsMock).toHaveReceivedCommand(PublishCommand);
expect(snsMock).toHaveReceivedCommandTimes(PublishCommand, 2);
expect(snsMock).toHaveReceivedCommandWith(PublishCommand, {Message: 'My message'});
expect(snsMock).toHaveReceivedNthCommandWith(2, PublishCommand, {Message: 'My message'});
```

https://github.com/m-radzikowski/aws-sdk-client-mock#jest-matchers

# 利用記録
## jest/ts-jest 準備
情報は多いので、使えるようにするだけのこと

```powershell: jest & ts-jest install
npm -D install jest ts-jest @types/jest
```
## aws-sdk-client-mock/aws-sdk-client-mock-jest 準備

```powershell: aws-sdk-client-mock(-jest)
npm install -D aws-sdk-client-mock aws-sdk-client-mock-jest
```

以下、Usage に従うだけ

https://github.com/m-radzikowski/aws-sdk-client-mock#usage

## lib mock
lib-dynamodb を使うので、以下のように Mock する
```typescript: 
import {
  GetUserCommand,
  CognitoIdentityProviderClient,
} from "@aws-sdk/client-cognito-identity-provider";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import {
  DynamoDBDocumentClient,
  DeleteCommand,
  GetCommand,
  PutCommand,
  QueryCommand,
  ScanCommand,
  UpdateCommand,
} from "@aws-sdk/lib-dynamodb";
import {
  S3Client,
  CreateMultipartUploadCommand,
  CompleteMultipartUploadCommand,
  DeleteObjectsCommand,
  PutObjectCommand,
  UploadPartCommand,
  PutPublicAccessBlockCommand,
} from "@aws-sdk/client-s3";
import { Upload } from "@aws-sdk/lib-storage";

import { mockClient } from "aws-sdk-client-mock";
import "aws-sdk-client-mock-jest";

const dynamoDB = new DynamoDBClient({});
const dynamoDBMock = mockClient(dynamoDB);
const userPoolMock = mockClient(CognitoIdentityProviderClient);
const dynamoDBdocMock = mockClient(DynamoDBDocumentClient);
const s3Mock = mockClient(S3Client);
```

## 各Command の返り値の設定
```typescript: Command Return
beforeEach(async () => {
  userPoolMock.reset();
  userPoolMock.onAnyCommand().resolves({});
  dynamoDBdocMock.reset();
  dynamoDBdocMock.onAnyCommand().resolves({});
  s3Mock.reset();
  s3Mock.onAnyCommand().resolves({});
});

test("テスト", async () => {
    userPoolMock
        .on(GetUserCommand)
        .resolves({ UserAttributes: [] });

    dynamoDBdocMock.onAnyCommand().resolves({}); // 全Command に対して定義
    dynamoDBdocMock
        .on(GetCommand) // GetCommand 全部
        .resolves({
          Item: [],
        })
        .on(GetCommand, {  // GetCommand Property の部分一致
          IndexName: "index-Age",
        })
        .resolves({
          Item: [],
        });

    s3Mock.on(CompleteMultipartUploadCommand).resolves(uploadInfo); 
    s3Mock.on(CreateMultipartUploadCommand).resolves({ UploadId: "1" });
    s3Mock.on(UploadPartCommand).resolves({ ETag: "1" });
});
```

https://github.com/m-radzikowski/aws-sdk-client-mock#order-of-mock-behaviors

:::note inof
同一コマンドで複数定義する場合は、先に広い定義をしておく。

> Wider Command matchers must be declared first, otherwise, they will take precedence over previous ones.

:::

## 確認例
```typescript: how to use 'expect'
expect(await lambdaHandler(events, context))
    .toStrictEqual(returnObjectExpected);

// QueryCommand の CommandInput を取得
const callsOfQuery = dynamoDBdocMock.commandCalls(QueryCommand);
expect(callsOfQuery).toHaveLength(1); // QueryCommand の Calls 数

// Mock 単位で *Command の Calls 順番毎に、input の確認
//  aws-sdk-client-mock-jest を利用した場合。
expect(dynamoDBdocMock).toHaveReceivedNthCommandWith(1, QueryCommand, {
    TableName: "target",
    IndexName: "index-target",
    ExpressionAttributeNames: {
      "#PK": "partition key",
      "#SK": "range key",
    },
    ExpressionAttributeValues: {
      ":PK": "pk value",
      ":startAfter": `${timestampBegin}`,
      ":endBefore": `${timestampEnd}`,
    },
    KeyConditionExpression:
      "#PK = :PK AND ( #SK BETWEEN :startAfter AND :endBefore )",
    Limit: limit,
    ScanIndexForward: false,
});
//  aws-sdk-client-mock-jest を利用しない場合、以下のようなやり方も。
//  property の部分確認が必要な場合にはこちらも必要
const queryCommandInput = callsOfQuery[0].args[0].input;  // 実際の input を取得
const queryItems = queryCommandInput.Items;  // 取得データ
expect(queryItems[0].timestamp.toString()).toMatch(/^\d{13}$/); // 正規表現での確認
```

# 躓きの記録と対処
## Mock が動かない
- 前提
Sam build/deploy する為に、Lambda 毎に階層化して実装してた。
この際、test.ts は 最上位に test/ フォルダを用意して作成していたところ、Mock ではなく 実 AWS 稼働になってた。
- 原因
正直よくわかってない。
- 解決策
Mock の Emulate 順序によるのかと考えて、Import などの位置を前後しても特に変わらなかったが、
たまたま、Lambda と同階層に test/ フォルダを移動したら Mock が効くようになった。

## 期待値が違う
- 原因
Mock の resolves の定義順序ミス
- 解決策
広域⇒狭域へを見直した

## toBe から toStrictEqual へ
failed メッセージに従って対処するのみ

# 参考
- jest

https://jestjs.io/ja/docs/configuration#automock-boolean

- aws-mock

https://aws.amazon.com/jp/blogs/developer/mocking-modular-aws-sdk-for-javascript-v3-in-unit-tests/

https://github.com/m-radzikowski/aws-sdk-client-mock


# あとがき
一旦 AWS 業務とはバイバイなのです。
次の業務はなんになるのやら :thinking: