proxyrequire で外部ライブラリをモック化してテストする

Mocha・Chai・Sinon を使ってユニットテストを書いている時、require() で読み込んだ外部ライブラリをモック化する必要が出た。

具体的にいうと、aws-sdk パッケージを使った Lambda 関数を UT するために、aws-sdk が実際には通信を行わないよう、モック化する必要があった。

コード中で require() しているパッケージをモック化するには、proxyrequire というパッケージを使うと簡単だった。

まず、テスト対象のコードはこんな感じ。

const awsSdk = require('aws-sdk');

const ssm = new awsSdk.SSM();

exports.handler = async (event, context) => {
  const secret = await ssm.getParameter({
    Name: 'my-param',
    WithDecryption: true
  }).promise();
  
  const secureString = secret.Parameter.Value;
  
  context.succeed({
    'My Secure String': secureString
  });
};

続いて、テストコードはこんな感じ。

const chai = require('chai');
const proxyquire = require('proxyquire');
const sinon = require('sinon');

const expect = chai.expect;

describe('ユニットテスト', () => {
  let myFunction = null;
  let event = null;
  let context = null;
  
  let mockSsm = null;
  let stubGetParameter = null;
  
  beforeEach(() => {
    event = {};  // 今回は適当に…
    
    context = {
      succeed: (result) => Promise.resolve(result)  // Lambda 関数終了時の値を Promise で返すようにしておく
    };
    
    // モッククラスを作っておく。処理は書かなくて良いが関数定義が必要
    mockSsm = class {
      getParameter(_params) { }
    };
    
    // ProxyRequire を使う
    myFunction = proxyquire('../src/my-function.js', {
      'aws-sdk': {  // require() で指定している文字列をそのまま指定する
        SSM: mockSsm  // モッククラスを注入する
      }
    });
  });
  
  it('正常系確認', (done) => {
    const testSecureString = 'Test Value';
    
    // モッククラスの関数を指定してフェイク処理を定義する
    // 戻り値とかはモック化対象のライブラリの仕様に合わせて頑張って組み立てる
    stubGetParameter = sinon.stub(mockSsm.prototype, 'getParameter').callsFake((params) => {
      expect(params).to.deep.equal({ Name: 'my-param', WithDecryption: true });
      return {
        promise: () => {
          return Promise.resolve({
            Parameter: {
              Value: testSecureString
            }
          });
        }
      };
    });
    
    myFunction.handler(event, context)  // 関数を実行する
      .then((result) => {
        expect(stubGetParameter.called).to.be.true;
        expect(result).to.deep.equal({
          'My Secure String': testSecureString
        });
        done();
      })
      .catch((error) => {
        done(error);
      });
  });
});

こんな感じ。

ProxyRequire の使用箇所は以下。

myFunction = proxyquire('../src/my-function.js', {
  'aws-sdk': {
    SSM: mockSsm
  }
});

内部実装を見てみると、require() 部分の実装を prototype から書き換えて、モック化した関数を注入しているようだ。

というか require() の実装が require('module') で拾えるの知らなかった…。

非同期関数と done() を組み合わせる際、it()async が使えなかったので、expect()done() での検証部分を普通の Promise で書いている。

こねこねするのがなかなか大変だったが、proxyrequire でちゃんとモックが注入できたのでよきよき。