こんなのがあったのか!? AWS SDK v3 Client mock

## はじめに

結構前の話になりますが、AWSのBlogを読み漁っていると、以下のような記事を見つけました。

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

どうやらJS用のAWS-SDKのクライアント用のMockライブラリがあるらしい。。。

というわけで、さっそく試してみることにした。

## **AWS SDK v3 Client mock**

詳細は、以下GitHubのリンクを参照ください。

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

## **使い方（例）**

「S3のバケットからオブジェクトを取得する処理」を例にしてMockライブラリを試してみようと思う。

### 例（S3のバケットからオブジェクトを取得する処理）

```ts
// src/main.ts
import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3'

export async function main() {
	try {
    const s3 = new S3Client({ region: 'ap-northeast-1' })
    const obj = await s3.send(
      new GetObjectCommand({
        Bucket: 'dummyBucket',
        Key: 'dummyKey',
      }),
    )
    if (obj.$metadata.httpStatusCode && obj.$metadata.httpStatusCode >= 400) {
      return 'NG'
    }
    return 'OK'
  } catch (error) {
    throw error
  }
}
```

### テストの例（S3のバケットからオブジェクトを取得する処理）
```ts
// test/main.spec.ts
import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3'
import { mockClient } from 'aws-sdk-client-mock'
import fs from 'fs'
import path from 'path'
import { main } from '../src/main'

const s3Mock = mockClient(S3Client)
beforeEach(() => {
  s3Mock.reset()
})

describe('S3のオブジェクトを取得できる', () => {
  it('成功', async () => {
    s3Mock
      .on(GetObjectCommand, {
        Bucket: 'dummyBucket',
        Key: 'dummyKey',
      })
      .resolves({
        $metadata: {
          httpStatusCode: 200,
        },
        Body: fs.createReadStream(path.resolve(__dirname, './example.txt')),
      })
    const result = await main()
    expect(result).toBe('OK')
  })

  it('失敗', async () => {
    s3Mock
      .on(GetObjectCommand, {
        Bucket: 'dummyBucket',
        Key: 'dummyKey',
      })
      .resolves({
        $metadata: {
          httpStatusCode: 400,
        },
        Body: fs.createReadStream(path.resolve(__dirname, './example.txt')),
      })
    const result = await main()
    expect(result).toBe('NG')
  })
})

describe('S3のオブジェクトを取得できない', () => {
  it('成功', async () => {
    s3Mock
      .on(GetObjectCommand, {
        Bucket: 'dummyBucket',
        Key: 'dummyKey',
      })
      .rejects('S3のオブジェクトを取得できない')

    // throwのチェックは以下の書き方が良さそう...
    const error = new Error('S3のオブジェクトを取得できない')
    await expect(main()).rejects.toEqual(error)
  })
})
```

今まで、Jestの`mockImplementation`や`mockImplementationOnce`を多用して、以下のようにAWS-SDKのクライアントをMockしていた時と比べるとだいぶコードが見やすくなった。

これは素晴らしい。。。

```ts
// Mockライブラリを使う前
jest.mock('@aws-sdk/client-s3')
mocked(S3Client).mockImplementation((): any => {
  return {
    send: (command: any): GetObjectCommandOutput => {
      return {
        $metadata: {
          httpStatusCode: 200,
        },
        Body: fs.createReadStream(path.resolve(__dirname, './example.txt')),
      }
    },
  }
})

// Mockライブラリを使った後
const s3Mock = mockClient(S3Client)
s3Mock
  .on(GetObjectCommand, {
    Bucket: 'dummyBucket2',
    Key: 'dummyKey2',
  })
  .resolves({
    $metadata: {
      httpStatusCode: 200,
    },
    Body: fs.createReadStream(path.resolve(__dirname, './example.txt')),
  })

```

特に便利だと思ったのが、`.on`にCommand系のパラメータを設定できるところ。

ドキュメントには「Order of mock behaviors」という見出しで書かれていましたが、要は`.on`のパラメータを見て、Mockの順番を変更できる機能。以下に例を書いてみる。

### 例（S3のバケットからオブジェクトを取得する処理2）
```ts
// src/main.ts
import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3'

export async function main() {
  try {
    const s3 = new S3Client({ region: 'ap-northeast-1' })

    // 1回目は、dummyBucket1からオブジェクトを取得		
    const obj1 = await s3.send(
      new GetObjectCommand({
        Bucket: 'dummyBucket1',
        Key: 'dummyKey1',
      }),
    )
    if (obj1.$metadata.httpStatusCode && obj1.$metadata.httpStatusCode >= 400) {
      return 'NG'
    }

    // 2回目は、dummyBucket2からオブジェクトを取得
    const obj2 = await s3.send(
      new GetObjectCommand({
        Bucket: 'dummyBucket2',
        Key: 'dummyKey2',
      }),
    )
    if (obj2.$metadata.httpStatusCode && obj2.$metadata.httpStatusCode >= 400) {
      return 'NG'
    }

    return 'OK'
  } catch (error) {
    throw error
  }
}
```

### テスト例（S3のバケットからオブジェクトを取得する処理2）
```ts
// test/main.spec.ts
import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3'
import { mockClient } from 'aws-sdk-client-mock'
import fs from 'fs'
import path from 'path'
import { main } from '../src/main'

const s3Mock = mockClient(S3Client)
beforeEach(() => {
  s3Mock.reset()
})

describe('S3のオブジェクトを取得できる', () => {
  it('成功', async () => {
    s3Mock
      .on(GetObjectCommand, {
        Bucket: 'dummyBucket2',
        Key: 'dummyKey2',
      })
      .resolves({
        $metadata: {
          httpStatusCode: 200, // dummyBucket2から取得した結果
        },
        Body: fs.createReadStream(path.resolve(__dirname, './example.txt')),
      })
      .on(GetObjectCommand, {
        Bucket: 'dummyBucket1',
        Key: 'dummyKey1',
      })
      .resolves({
        $metadata: {
          httpStatusCode: 200, // dummyBucket1から取得した結果
        },
        Body: fs.createReadStream(path.resolve(__dirname, './example.txt')),
      })

    const result = await main()
    expect(result).toBe('OK')
  })
})
```

複数回クライアントをMockする時に、どの結果がどのMockなのかが、とても見やすくなった。

## さいごに
個人的な感想だが、最近はAWS CDKを使うようになってLambdaのコード等もTypeScriptを使って書く機会が増えた。
TypeScriptは、テストコードを書く時に(Jestを使って）自力Mockも可能だが、やはりMock用のライブラリを使ったほうがコードが見やすくなるので、積極的に使っていきたい。