jestを使ってテストを書いていて、環境変数をケースごとに書き換えたくなった際にハマったので記事に残しておく。
前提
Lambdaで環境変数を受け取る際に、以下のようにhandlerの外側で変数を定義しているときの話である。
例)
import { Context, SQSEvent, SQSHandler } from 'aws-lambda';
import * as log from 'lambda-log';
// ** こんな感じで環境変数を取得している場合 **
export const TABLE_NAME = process.env.TABLE_NAME || '';
export const PRIMARY_KEY = process.env.PRIMARY_KEY || '';
export const handler: SQSHandler = async (event: SQSEvent, context: Context) => {
log.info('event row data', { event, context });
const instance = DynamoDBDocumentClient.from(new DynamoDBClient({ region: REGION }));
for (const record of event.Records) {
const body = JSON.parse(record.body);
log.info('event body', { body });
const params: PutCommandInput = {
TableName: TABLE_NAME,
Item: {
[PRIMARY_KEY]: messageId,
},
};
await instance.send(new PutCommand(params));
}
};
tl;dr
- テスト対象のコードを動的インポートする。
-
beforEach
の中でjest.resetModules()
を呼ぶ。 - 各テストの中で
process.env.XXX
にテスト用の値を代入する。
解説
jest
でテストを記載するときは、以下のようにテスト対象のファイルをコードの上部でインポートすると思う。
handler.test.ts
import { handler } from '../src/handler.ts';
describe('handlerのテスト', () => {
test('正常系', async () => {
const testEvent = { ... }
const result = await handler(testEvent);
expect(result).toBeTrythy();
});
});
しかし、このような書き方をすると、環境変数を変更したテストができない。
handler.test.ts
import { handler } from '../src/handler.ts';
describe('handlerのテスト', () => {
test('正常系', async () => {
const testEvent = { ... }
// これでは書き換わらず、想定している動きにならない。
process.env.TABLE_NAME = 'dummyValue';
const result = await handler(testEvent);
expect(result).toBeTrythy();
});
});
読み込まれるタイミングと動的インポート
この場合、環境変数はimport
したタイミングで解決されてしまう。
このため、handler.test.ts
を実行した最初のタイミングで環境変数が決まることになる。
ES Modulesの場合、動的インポートを使えば、テストケースの中で都度インポートできる。
handler.test.ts
- import { handler } from '../src/handler.ts';
describe('handlerのテスト', () => {
test('正常系', async () => {
const testEvent = { ... }
process.env.TABLE_NAME = 'dummyValue';
- const result = await handler(testEvent);
+ const module = await import('../src/handler.ts');
+ await module.handler(testEvent)
expect(result).toBeTrythy();
});
});
ES Modulesの場合のrequire.cache
の削除
上記でうまく動作するかと思いきや、一度目のインポートからはやはり環境変数が変更できないことに気づく。
handler.test.ts
describe('handlerのテスト', () => {
test('正常系', async () => {
const testEvent = { ... }
process.env.TABLE_NAME = 'dummyValue';
const module = await import('../src/handler.ts');
await module.handler(testEvent)
// この時点ではTABLE_NAME = 'dummyValue'
expect(result).toBeTrythy();
});
test('正常系2', async () => {
const testEvent = { ... }
process.env.TABLE_NAME = 'fakeValue';
const module = await import('../src/handler.ts');
await module.handler(testEvent)
// このタイミングでもTABLE_NAME = 'dummyValue'になってしまう
expect(result).toBeTrythy();
});
});
これは、ES Modulesの仕様上importがキャッシュされることに起因するらしい。
そこで以下のように、beforeEach
などでキャッシュを削除するとうまく動作する。
handler.test.ts
describe('handlerのテスト', () => {
+ beforeEach(() => {
+ jest.resetModules();
+ });
test('正常系', async () => {
const testEvent = { ... }
process.env.TABLE_NAME = 'dummyValue';
const module = await import('../src/handler.ts');
await module.handler(testEvent)
// この時点ではTABLE_NAME = 'dummyValue'
expect(result).toBeTrythy();
});
test('正常系2', async () => {
const testEvent = { ... }
process.env.TABLE_NAME = 'fakeValue';
const module = await import('../src/handler.ts');
await module.handler(testEvent)
// このタイミングではちゃんとTABLE_NAME = 'fakeValue'になっている
expect(result).toBeTrythy();
});
});
おまけ
環境変数のテストをする際は、describe
のスコープ内で変数を退避しておいてあげるとよさそうだ。
handler.test.ts
describe('handlerのテスト', () => {
+ const env = proccess.env
beforeEach(() => {
jest.resetModules();
+ process.env = { ...env };
});
test('正常系', async () => {
const testEvent = { ... }
process.env.TABLE_NAME = 'dummyValue';
const module = await import('../src/handler.ts');
await module.handler(testEvent)
expect(result).toBeTrythy();
});
test('正常系2', async () => {
const testEvent = { ... }
process.env.TABLE_NAME = 'fakeValue';
const module = await import('../src/handler.ts');
await module.handler(testEvent)
expect(result).toBeTrythy();
});
});