こんにちは、CX事業本部 Delivery部の若槻です。
前回のエントリで、再利用したいデータをAWSLambdaのグローバル変数でキャッシュするhandlerコードを紹介しました。
その後そのhandlerコードの単体テストをJestで作成しようとしたのですが、テスト対象モジュールの外にあるグローバル変数をモックさせたい場合には工夫が必要だったため、方法を書き残しておきます。
グローバル変数をモックしない場合、テストが期待通り動かない
前回のhandlerコードを少し改変した以下のコードのテストを作成してみます。処理内容は同じですが、テストのしやすさためにAWS SDKによるGetParameter処理を別関数に分けています。
lib/cdk-sample-app.nyaoFunc.ts
import { SSMClient, GetParameterCommand, Parameter } from '@aws-sdk/client-ssm';
export let HOGE_CACHE: string | undefined;
export const handler = async (): Promise<string> => {
if (HOGE_CACHE !== undefined) {
return HOGE_CACHE;
}
const parameter = await getParameter();
if (parameter !== undefined && parameter!.Value !== undefined) {
const hoge = parameter.Value;
HOGE_CACHE = hoge;
return hoge;
}
HOGE_CACHE = undefined;
throw new Error('Parameter value is invalid.');
};
const ssmClient = new SSMClient({
region: 'ap-northeast-1',
});
export const getParameter = async (): Promise<Parameter | undefined> => {
const response = await ssmClient.send(
new GetParameterCommand({ Name: 'hoge' })
);
return response.Parameter;
};
上記コードが期待通りの動作となるのかを確認するために、Jestで次のような3つの単体テストケースを作成してみました。
test/cdk-sample-app.nyaoFunc.test.ts
import { handler, getParameter, HOGE_CACHE } from '../lib/cdk-sample-app.nyaoFunc';
describe('キャッシュされていない場合', (): void => {
describe('パラメーターがParameter Storeから取得できた場合', (): void => {
test('グローバル変数にキャッシュされ、リターンされる', async (): Promise<void> => {
(getParameter as jest.Mock) = jest
.fn()
.mockReturnValue({ Value: 'あああ' });
expect(HOGE_CACHE).toBeUndefined();
const response = await handler();
expect(getParameter).toBeCalledTimes(1);
expect(response).toBe('あああ');
expect(HOGE_CACHE).toBe('あああ');
});
});
describe('パラメーターがParameter Storeから取得できなかった場合', (): void => {
test('グローバル変数からキャッシュが削除され、エラー終了する', async (): Promise<void> => {
(getParameter as jest.Mock) = jest
.fn()
.mockReturnValue({ Value: undefined });
expect(HOGE_CACHE).toBeUndefined();
await expect(() => handler()).rejects.toThrow(
'Parameter value is invalid.'
);
expect(getParameter).toBeCalledTimes(1);
expect(HOGE_CACHE).toBeUndefined();
});
});
});
describe('キャッシュされている場合', (): void => {
test('パラメーターがグローバル変数から取得される', async (): Promise<void> => {
(getParameter as jest.Mock) = jest
.fn()
.mockReturnValue({ Value: 'あああ' });
const response = await handler();
expect(getParameter).toBeCalledTimes(0);
expect(response).toBe('いいい');
expect(HOGE_CACHE).toBe('いいい');
});
});
しかしテストを実行してみると、テスト毎にグローバル変数HOGE
の値がundefined
にリセットされて欲しかったのですが、1つ目のテストケースでグローバル変数に格納されたあああ
が後続のテストケースで使い回されてしまっています。
$ npx jest test/cdk-sample-app.nyaoFunc.test.ts
FAIL test/cdk-sample-app.nyaoFunc.test.ts
キャッシュされていない場合
パラメーターがParameter Storeから取得できた場合
✓ グローバル変数にキャッシュされ、リターンされる (1 ms)
パラメーターがParameter Storeから取得できなかった場合
✕ グローバル変数からキャッシュが削除され、エラー終了する
キャッシュされている場合
✕ パラメーターがグローバル変数から取得される (2 ms)
● キャッシュされていない場合 › パラメーターがParameter Storeから取得できなかった場合 › グローバル変数からキャッシュが削除され、エラー終了する
expect(received).toBeUndefined()
Received: "あああ"
30 | .mockReturnValue({ Value: undefined });
31 |
> 32 | expect(HOGE_CACHE).toBeUndefined();
| ^
33 |
34 | await expect(() => handler()).rejects.toThrow(
35 | 'Parameter value is invalid.'
at Object.<anonymous> (test/cdk-sample-app.nyaoFunc.test.ts:32:26)
● キャッシュされている場合 › パラメーターがグローバル変数から取得される
expect(received).toBe(expected) // Object.is equality
Expected: "いいい"
Received: "あああ"
53 | expect(getParameter).toBeCalledTimes(0);
54 |
> 55 | expect(response).toBe('いいい');
| ^
56 |
57 | expect(HOGE_CACHE).toBe('いいい');
58 | });
at Object.<anonymous> (test/cdk-sample-app.nyaoFunc.test.ts:55:22)
Test Suites: 1 failed, 1 total
Tests: 2 failed, 1 passed, 3 total
Snapshots: 0 total
Time: 2.761 s, estimated 3 s
Ran all test suites matching /test\/cdk-sample-app.nyaoFunc.test.ts/i.
グローバル変数を操作する関数を設けてモックした場合、テストが期待通り動いた
そこでhandlerコードにグローバル変数から値を取得および設定する関数getHogeCache
とsetHogeCache
を設けて、handler内ではその関数を使用してキャッシュを操作するようにします。
lib/cdk-sample-app.nyaoFunc.ts
import { SSMClient, GetParameterCommand, Parameter } from '@aws-sdk/client-ssm';
export let HOGE_CACHE: string | undefined;
export const getHogeCache = (): string | undefined => HOGE_CACHE;
export const setHogeCache = (newParameter: string | undefined): void => {
HOGE_CACHE = newParameter;
};
export const handler = async (): Promise<string> => {
const hogeCache = getHogeCache();
if (hogeCache !== undefined) {
return hogeCache;
}
const parameter = await getParameter();
if (parameter !== undefined && parameter!.Value !== undefined) {
const hoge = parameter.Value;
setHogeCache(hoge);
return hoge;
}
setHogeCache(undefined);
throw new Error('Parameter value is invalid.');
};
const ssmClient = new SSMClient({
region: 'ap-northeast-1',
});
export const getParameter = async (): Promise<Parameter | undefined> => {
const response = await ssmClient.send(
new GetParameterCommand({ Name: 'hoge' })
);
return response.Parameter;
};
テストコード側ではgetHogeCache
とsetHogeCache
をモックします。
test/cdk-sample-app.nyaoFunc.test.ts
import {
handler,
getParameter,
getHogeCache,
setHogeCache,
} from '../lib/cdk-sample-app.nyaoFunc';
describe('キャッシュされていない場合', (): void => {
describe('パラメーターがParameter Storeから取得できた場合', (): void => {
test('グローバル変数にキャッシュされ、リターンされる', async (): Promise<void> => {
(getParameter as jest.Mock) = jest
.fn()
.mockReturnValue({ Value: 'あああ' });
(getHogeCache as jest.Mock) = jest.fn().mockReturnValue(undefined);
(setHogeCache as jest.Mock) = jest.fn().mockReturnValue(undefined);
const response = await handler();
expect(getParameter).toBeCalledTimes(1);
expect(response).toBe('あああ');
expect(setHogeCache).toBeCalledTimes(1);
expect(setHogeCache).toBeCalledWith('あああ');
});
});
describe('パラメーターがParameter Storeから取得できなかった場合', (): void => {
test('グローバル変数からキャッシュが削除され、エラー終了する', async (): Promise<void> => {
(getParameter as jest.Mock) = jest
.fn()
.mockReturnValue({ Value: undefined });
(getHogeCache as jest.Mock) = jest.fn().mockReturnValue(undefined);
(setHogeCache as jest.Mock) = jest.fn().mockReturnValue(undefined);
await expect(() => handler()).rejects.toThrow(
'Parameter value is invalid.'
);
expect(getParameter).toBeCalledTimes(1);
expect(setHogeCache).toBeCalledTimes(1);
expect(setHogeCache).toBeCalledWith(undefined);
});
});
});
describe('キャッシュされている場合', (): void => {
test('パラメーターがグローバル変数から取得される', async (): Promise<void> => {
(getParameter as jest.Mock) = jest
.fn()
.mockReturnValue({ Value: 'あああ' });
(getHogeCache as jest.Mock) = jest.fn().mockReturnValue('いいい');
(setHogeCache as jest.Mock) = jest.fn().mockReturnValue(undefined);
const response = await handler();
expect(getParameter).toBeCalledTimes(0);
expect(response).toBe('いいい');
expect(setHogeCache).toBeCalledTimes(0);
});
});
テストを実行すると、すべてPASSさせることができました。ちゃんとモックさせられているようです!
$ npx jest test/cdk-sample-app.nyaoFunc.test.ts
PASS test/cdk-sample-app.nyaoFunc.test.ts
キャッシュされていない場合
パラメーターがParameter Storeから取得できた場合
✓ グローバル変数にキャッシュされ、リターンされる (2 ms)
パラメーターがParameter Storeから取得できなかった場合
✓ グローバル変数からキャッシュが削除され、エラー終了する (5 ms)
キャッシュされている場合
✓ パラメーターがグローバル変数から取得される
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 2.984 s, estimated 3 s
Ran all test suites matching /test\/cdk-sample-app.nyaoFunc.test.ts/i.
念の為、修正後のhandlerコードをAWS Lambdaにデプロイして、前回作成したE2Eテストのコードを動かしてみると、こちらもPASSしました。ウォームスタート時に上手くキャッシュが効いているようです!
$ npx jest test/cdk-sample-app.nyaoFunc.e2e.test.ts
PASS test/cdk-sample-app.nyaoFunc.e2e.test.ts (10.813 s)
✓ コールドスタート (4204 ms)
✓ ウォームスタート (3130 ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 10.875 s, estimated 11 s
Ran all test suites matching /test\/cdk-sample-app.nyaoFunc.e2e.test.ts/i.
おわりに
Jestでグローバル変数を扱うモジュールの単体テストをする方法を確認しました。
結論としては、グローバル変数を操作する関数を設けてモックするようにしましょう。
以上