6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【JavaScript/TypeScript】jestで環境変数を差し替えてテストする方法

Posted at

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();
  });
});
6
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?