jestでaws-sdk、aws-xray-sdkをmock化する方法

## jestでaws-sdkのmockを作る
jestとはFacebookによる、jsのユニットテスト用モジュールです。
非常に簡潔なコードでユニットテストを気軽に実装できるので重宝しようと導入したのですが、初心者だと外部モジュールをmock化するには意外と手間取ったりします。

例えば、Lambdaを使用する中でaws-sdkや、それをaws-xray-sdkでラップした構造体を扱ったコードをテストしたい場合、aws-sdkのモジュールをmockにしなければなりません。（SAMを使えば違う書き方もできるはずです。SAMに関しては別記事にてご確認ください。）

そんなわけでmock化するためにいろいろと苦労したのですが、その中で一つ発見があったので記事にしました。
ひとまず細かい解説は抜きにして、手本の一つとしてご覧ください。これでaws-sdkのモジュールは確実にmockにできます。記述にご不明点などあればご質問いただくか、[**Jestのドキュメント**](https://jestjs.io/ja/)をご覧ください。（後者の方が確実です）

## aws-sdkのモジュールをmock化するための基本
テストする対象のコード例

```typescript
import * as AWS from 'aws-sdk';

const s3 = new AWS.S3({ region: 'ap-northeast-1' });
const dynamo = new AWS.DynamoDB.DocumentClient({ region: 'ap-northeast-1' });

export async function getObjectFromS3(Bucket: string, Key: string) {
    return await s3.getObject({ Bucket, Key }).promise();
}

export async function putItemDynamo(TableName: string, Item: {[key: string]: any}): Promise<AWS.DynamoDB.PutItemOutput> {
    return await dynamo.put({ TableName, Item }).promise();
}
```

上記のコードではS3の特定のパスからファイルデータを取得する`getObjectFromS3`と、DynamoDBにデータを追加する`putItemDynamo`を実装しています。これら２つには明確な違いがあります。AWS直下のclassから関数を呼び出しているか、さらに一階層深いクラスから呼び出しているかという違いです。
どちらもクラスであり、関数の呼び出し方にも違いがないはずなのに、なぜかmockの作り方に違いがあるのでそれが記事の主題になります。

また、AWSのモジュール呼び出しは`.promise()`がつくことになると思いますが、それへの対策も下記に含まれているのでご参考までに。

```typescript:jestコード
import assert = require('assert');
import { mocked } from 'ts-jest/utils';
import * as AWS from 'aws-sdk';
import { getObjectFromS3, putItemDynamo } from './index.ts'

jest.mock('aws-sdk');

it('getObjectFromS3', async () => {
    const getMock = AWS.S3.prototype.getObject = jest.fn().mockReturnValueOnce({
        promise: jest.fn().mockResolvedValueOnce({
            dummy: true
        })
    })

    const res = await getgetObjectFromS3('dummy-bucket', 'test/test.csv');

    assert.deepStrictEqual(res, { dummy: true });
    assert(getMock.mock.calls[0][0] == 'dummy-bucket');
    assert(getMock.mock.calls[0][1] == 'test/test.csv');
})

it('putItemDynamo', async () => {
    const putMock = AWS.DynamoDB.DocumentClient.prototype.put as jest.Mock;
    mocked(putMock).mockReturnValueOnce({
        promise: jest.fn().mockResolvedValueOnce({
            status: 'succeed'
        })
    })

    const res = await putItemDynamo('dummy_table', {
        id: 0,
        name: 'hoge'
        address: 'fuga'
    })

    assert.deepStrictEqual(res, {
        status: 'succeed'
    });
    assert(putMock.mock.calls[0][0] == 'dummy_table');
    assert.deepStrictEquals(putMock.mock.calls[0][1], {
        id: 0,
        name: 'hoge'
        address: 'fuga'
    });
})
```

注意点は二つあります。
一つ目として、`DynamoDB.DocumentClient`内の関数をmockにする場合には`AWS.DynamoDB.DocumentClient.prototype.〇〇 as jest.Mock`で、それ以外で、`aws-sdk`直下のクラスをmockにする場合には`AWS.〇〇.prototype.〇〇 = jest.fn().〜`のように記述すると。
これだけ抑えておけばmock作る際のストレスがかなり低減できます。
次に`.promise()`を含めたmock化。ここの書き方は比較的自由なんですが、ここではjest.fn()でモックにして扱っています。

## 【参考】Xrayも使う場合のコード例

```typescript
import * as aws from 'aws-sdk';
import AWSXray = require('aws-xray-sdk');

const AWS = AWSXray.captureAWS(aws); // aws-sdkのモジュールをラップする関数

const s3 = new AWS.S3({ region: 'ap-northeast-1' });
const dynamo = new AWS.DynamoDB.DocumentClient({ region: 'ap-northeast-1' });

export async function getObjectFromS3(Bucket: string, Key: string) {
    return await s3.getObject({ Bucket, Key }).promise();
}

export async function putItemDynamo(TableName: string, Item: {[key: string]: any}): Promise<AWS.DynamoDB.PutItemOutput> {
    return await dynamo.put({ TableName, Item }).promise();
}
```

```typescript:jestコード
import assert = require('assert');
import { mocked } from 'ts-jest/utils';
import * as AWS from 'aws-sdk';
import { getObjectFromS3, putItemDynamo } from './index.ts'

jest.mock('aws-sdk');

jest.mock('aws-xray-sdk', () => {
    return {
        captureAWS: (aws: any) => aws
    }
})

it('getObjectFromS3', async () => {
    const getMock = AWS.S3.prototype.getObject = jest.fn().mockReturnValueOnce({
        promise: jest.fn().mockResolvedValueOnce({
            dummy: true
        })
    })

    const res = await getgetObjectFromS3('dummy-bucket', 'test/test.csv');

    assert.deepStrictEqual(res, { dummy: true });
    assert(getMock.mock.calls[0][0] == 'dummy-bucket');
    assert(getMock.mock.calls[0][1] == 'test/test.csv');
})

it('putItemDynamo', async () => {
    const putMock = AWS.DynamoDB.DocumentClient.prototype.put as jest.Mock;
    mocked(putMock).mockReturnValueOnce({
        promise: jest.fn().mockResolvedValueOnce({
            status: 'succeed'
        })
    })

    const res = await putItemDynamo('dummy_table', {
        id: 0,
        name: 'hoge'
        address: 'fuga'
    })

    assert.deepStrictEqual(res, {
        status: 'succeed'
    });
    assert(putMock.mock.calls[0][0] == 'dummy_table');
    assert.deepStrictEquals(putMock.mock.calls[0][1], {
        id: 0,
        name: 'hoge'
        address: 'fuga'
    });
})
```

ここで重要なのは`jest.mock('aws-xray-sdk', () => {return { captureAWS: (aws: any) => aws }})`の件。これによって型定義のない厄介者であるaws-xray-sdkを無視して扱うことが可能になります。

## 総括
これでaws-sdkのモジュールをmockにするのは難しくなくなりますね。他にも多くのモジュールがありますが、DocumetClientパターンか他のパターンかで大体mockを生成できるので、使ってやってください〜。