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のドキュメントをご覧ください。(後者の方が確実です)

aws-sdkのモジュールをmock化するための基本

テストする対象のコード例

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を実装しています。これら2つには明確な違いがあります。AWS直下のclassから関数を呼び出しているか、さらに一階層深いクラスから呼び出しているかという違いです。
どちらもクラスであり、関数の呼び出し方にも違いがないはずなのに、なぜかmockの作り方に違いがあるのでそれが記事の主題になります。

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

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も使う場合のコード例

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();
}
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を生成できるので、使ってやってください〜。