7
2

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.

この記事誰得? 私しか得しないニッチな技術で記事投稿!

Node.js Test Runnerとビルトインモジュールのみでモジュールモックするヘルパーをつくった

Posted at

はじめに

以下の記事で、Node.js Test Runnerで、モジュールのサブセットをモックする方法を書きました。

しかし、サブセットでないモジュールのモックもしたいときがあるなと思い調べたところ、モジュールモックをできる簡易的なヘルパーができたので紹介します。

なお、今回試すのはCJSです。

ヘルパーを使ってテストする

サブセットでないモジュール(正しい名前がわからない)と言っているのは以下のようなものです。

add.js
module.exports = (a, b) => {
  return a + b;
};

今回は、 node-fetch で試します。

テストしたいものは以下のgetExampleBody関数です。

index.js
const fetch = require("node-fetch");

exports.getExampleBody = async () => {
  const response = await fetch("https://example.com");
  const body = await response.text();
  return body;
};

このgetExampleBodyをテストする際に、fetchでのリクエストをさせたくないため、fetch関数をモックしたいです。

の記事で書いたように、サブセットのモジュールは mock.method でモックができるのですが、今回の場合はできません。

そこで思いついたのが、モジュールのロードに介入して、モック関数を返す方法です。
実際に書いたヘルパーは以下です。

module-mock-helper.js
const Module = require("node:module");

const mocks = {};

const originalJsLoader = Module._extensions[".js"];
Module._extensions[".js"] = function (mod, filename) {
  if (mocks[filename]) {
    mod.exports = mocks[filename];
  } else {
    originalJsLoader(mod, filename);
  }
};

exports.mockModule = (name, fn) => {
  const filename = require.resolve(name);
  mocks[filename] = fn;
  delete require.cache[filename];

  return fn;
};

使い方は簡単で、mockModuleの第一引数にモジュール名、第二引数にモック関数を渡すだけです。

このヘルパーを使って書いたテストコードは以下になります。

index.test.js
const { test, mock } = require("node:test");
const assert = require("node:assert");
const { mockModule } = require("./module-mock-helper");
const fetch = require("node-fetch");

const mockFetch = mockModule(
  "node-fetch",
  mock.fn(fetch, async () => {
    return {
      text: async () => {
        return "sample body";
      },
    };
  }),
);

const { getExampleBody } = require("./index");

test("getExampleBody with mock", async () => {
  const body = await getExampleBody();
  assert.equal(body, "sample body");
  assert.equal(mockFetch.mock.callCount(), 1);
});

mockModuleの仕組みは、モジュールのロードに介入し、モック関数を返すようにしています。

mockModuleで渡されたモジュール名からパスを取得し、そのパスをkeyとして渡されたモック関数を記憶しておきます。
そして、Module._extensionsをオーバーライドし、記憶しているモジュールをロードしようとした際にモック関数を返すようにしています。

さいごに

無事、ビルトインモジュールだけでモジュールモックができました。
ただ、Native ESMの場合のモジュールロード周りがわからないので、次回挑戦するならESMで挑戦してみようと思います。

Refs

7
2
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
7
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?