以下の内容はhttps://kiririmode.hatenablog.jp/entry/20250112/1736636122より取得しました。


fs.existsSyncをモックするためにproxyquireを使う

Typescriptのプロダクションコードでfs.existSyncを使うようにした。Node.jsのAPIなんでそれは当然動くんだけど、このコードをテストしたい。テストするために、fs.existSyncをsinonでモックしようとしたところ次のようなエラーになった。

existsSyncStub = sinon.stub(fs, "existsSync");
// TypeError: Descriptor for property existsSync is non-configurable and non-writable

non-configurable and non-writable とは?

Javascriptでは、writableとかconfigurableとかのデータプロパティを設定できるようになっており、それがfalseに設定されると代入不可や属性変更不可にするといった制御ができる。

fs.existsSyncはそういった設定がされているようだ。 これがなぜなのかというと、主としてセキュリティ観点と思われる。まぁ外部から容易にコアモジュール実装を入れ替えることが可能だと、それがセキュリティリスクになるよねというのもうなづける。

それでもモックしたい時の proxyquire

でもテストしたいし、テストするためにfs.existsSyncをモックしたい。そういう時にどうすれば良いのか調べたところ、proxyquireというモジュールを使うと良いらしい。実際、util.tsにあるhogeという関数をテストするために、fs.existsSyncをモックするコードは次のようになり、これでテストができるようになった。

const fsStub = {
  existsSync: sinon.stub(),
};
const { hoge } = proxyquire("../util", { fs: fsStub });

suite("hoge テスト", () => {
    setup(() => {
      fsStub.existsSync.reset();
    });

    test("hoge〜〜", () => {
      const dirPath = "/absolute/path/to/dir";
      fsStub.existsSync.withArgs(dirPath).returns(true);
      const result = hoge(dirPath)
      assert.strictEqual(result.path, dirPath);
    });
    (略)
});

proxyquireは何をしているの?

なんかすごく黒魔術的な感じがしたので、proxyquireが何をしているのか調べてみた。

結論から言うと、proxyquirerequireをラップとして、モックしたいモジュールを差し替えるということをしている。 今回のケースで言うと、fsモジュールを require する際に、existsSyncに対してはスタブを差し込むようにしている。

具体的な実装はこうなっている

Proxyquire.prototype._overrideExtensionHandlers = function (module, resolvedStubs) {
  /* eslint node/no-deprecated-api: [error, {ignoreGlobalItems: ["require.extensions"]}] */
  var originalExtensions = {}
  var self = this

  Object.keys(require.extensions).forEach(function (extension) {
    // Store the original so we can restore it later
    if (!originalExtensions[extension]) {
      originalExtensions[extension] = require.extensions[extension]
    }

    // Override the default handler for the requested file extension
    require.extensions[extension] = function (module, filename) {
      // Override the require method for this module
      module.require = self._require.bind(self, module, resolvedStubs)

      return originalExtensions[extension](module, filename)
    }
  })

  // Return a function that will undo what we just did
  return function () {
    Object.keys(originalExtensions).forEach(function (extension) {
      require.extensions[extension] = originalExtensions[extension]
    })
  }
}

require.extensionsと言うのが鍵で、このNode.jsのAPIは、元々は特定の拡張子のファイルがrequireされた時の挙動をカスタマイズするためのものとして用意されている。

Instruct require on how to handle certain file extensions.

Modules: CommonJS modules | Node.js v23.6.0 Documentation

proxyquire はこの機構を利用して、requireの挙動自体を書き換えており、モジュールをロードする際に、指定されたモックを差し込むようにしている。こうすると、確かに「書き換え」は発生しないので、冒頭のエラーも発生しない。裏技感ある。




以上の内容はhttps://kiririmode.hatenablog.jp/entry/20250112/1736636122より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14