セットプチフォッカ

勉強したアウトプット、ときどきフォッカチオ作っていました

Node.jsでファイルが直接実行されたときだけ処理を呼び出す方法

f:id:ikmbear:20210819002958p:plain

経緯

JavaScriptの練習として、FizzBuzz問題を解いており、ついでにJestでテストも書いていました。

// 実行ファイル:fizzbuzz.js
// fizzbuzz関数
const fizzbuzz = (num) => {
    ...
}

// 表示処理
for (...) {
    console.log(fizzbuzz(num))
}

module.exports = fizzbuzz
// テストファイル:fizzbuzz.spec.js
const fizzbuzz = require('./fizzbuzz')

describe('FizzBuzz:1~20', () => {
  test('3の倍数の場合には「Fizz」を返す', () => {
    // テスト処理
  })
  ...
})

テスト自体はパスするようにできたのですが、テスト結果にも実行ファイルに書かれているconsole.logの結果が表示されてしまう始末。これをどうにか表示させないようにするため、色々と調べてみました。

# テスト実行
% npm test

> test
> jest

PASS ./fizzbuzz.spec.js
  FizzBuzz:1~20
    ✓ 3の倍数の場合には「Fizz」を返す (1 ms)
    ✓ 5の倍数の場合には「Buzz」を返す
    ✓ 3と5両方の倍数の場合には「FizzBuzz」を返す
    ✓ そのほかの数の時はそのままの数を返す (1 ms)

# ==========テストとは関係ないので表示したくない(ここから)==========
  console.log
    1

      at Object.<anonymous> (fizzbuzz.js:18:11)

  console.log
    2

      at Object.<anonymous> (fizzbuzz.js:18:11)
# ==========テストとは関係ないので表示したくない(ここまで)==========

Test Suites: 1 passed, 1 total
Tests:       4 passed, 4 total
Snapshots:   0 total
Time:        0.488 s, estimated 1 s
Ran all test suites.

TL:DR;

Node.jsでファイルが直接実行されているかどうかは、require.main === moduleで判断することができる。 参考:https://nodejs.org/api/modules.html#modules_accessing_the_main_module

if (require.main === module) {
    // 直接呼び出された場合にだけ実行したい処理
}

Ruby__FILE__ = $PROGRAM_NAME的なイディオムがないか探してみる

やりたいことは次の2つです。 - 実行ファイル(fizzbuzz.js)を実行した際には、console.logの結果を表示したい - 別のファイルから呼ばれた際には、console.logを表示したくない

Rubyの場合、以下のように記述することで、そのファイルが直接実行されたときだけ、特定の処理を呼び出すことができます。

if __FILE__ = $PROGRAM_NAME
    # 直接呼び出された場合だけ実行したい処理
end

JavaScriptでも同じような処理ができないかと思い、「FILE JavaScript」で検索してみました。
しかし、JavaScript__FILE__はファイルの絶対パスを参照し、また$PROGRAM_NAMEに相当するものも見つからなかったため、一度この方向性は断念しました。

fjordbootcamp内で質問してみる

上記経緯からfjordboocamp内で質問してみたところ、「『Jest console.log』でググると、console.logをMock化しろと出ているよ」との声をいただきました。

先ほどは実行ファイルの方で表示を制御する方法で検討していましたが、テストファイルで表示を制御する方法を提示いただいたわけです。

Jestでconsole.logをMock化してみる

「Jest console.log」で検索した見つかった、下記記事を参考にMock化を図りました。

Jestでconsole.logをモックする. 最近はテストを書くときはほとんどJestを使っています。今回は、v19から導入さ… | by 赤芽 | Medium

console.logの結果が残っています。これは、mockObj.mockImplementation(() => customImplementation) かobject[methodName] = jest.fn(() => customImplementation) を使うことで上書きできます。

Jestのドキュメント(jest.spyOn)にも記載されているとおり、以下の記述でconsoleオブジェクトのlogメソッドを別の処理(x => xつまりなにもしない)に置き換えることができます。

// consoleオブジェクトのlogメソッドをMock化して、その処理を`x => x`に置き換える
jest.spyOn(global.console, 'log').mockImplementation(x => x);

しかしながら、テスト実行結果には変わらずconsole.logの結果が出力されています。
Mock化とメソッドの上書きが正しく実行されているのか、それとも置き換え対象のメソッドがないのかの切り分けのため、以下のようにconsole.logが呼ばれたかどうかをテストに追記します。

const fizzbuzz = require('./fizzbuzz')

describe('FizzBuzz:1~20', () => {
  test('3の倍数の場合には「Fizz」を返す', () => {
    // テスト処理
    // 追記箇所:console.logが呼ばれているか確認
    expect(console.log).toBeCalled()
  })
  ...
})

この結果はテスト失敗、つまりconsole.logは呼ばれていないこととなり、テストケースで呼ばれていない処理をMock化しても意味がないことがわかりました。

console.logが何によって実行されているか確認してみる

テストケース内で呼ばれていないことがわかったため、消去法的に実行ファイルfizzbuzz.jsrequireした瞬間にconsole.logが実行されていると推測できます。

(ちょっとここらへんどう検索したか忘れましたが、)実際にrequireは実行された瞬間にそのファイルを読み込む仕様とSなっており、これを回避するための策として、以下の手順が書かれている記事をいくつか見つけました。

if (!module.parent)
    // 直接呼び出された時だけ実行される処理
end

module.parentはrequire元であり、自身がrequireされていれば、falsyになるという仕組みです。

module.parentの代替案を探す

いずれの記事も更新日時が古かったため、Node.jsの公式ドキュメントを確認すると、module.parentは非推奨となっていました。

Deprecated: Please use require.main and module.children instead.

Modules: CommonJS modules | Node.js v16.7.0 Documentation

代わりにrequire.mainmodule.childrenを利用するように記載があります。
require.mainを確認すると、参考リンクに探し求めていた解決方法がありました😄

When a file is run directly from Node.js, require.main is set to its module. Modules: CommonJS modules | Node.js v16.7.0 Documentation

「Node.jsから直接ファイルが実行されると、require.mainには、自身のmoduleが設定される。」 つまり、次のように記述することで要件を満たすことができたのでした。

if (require.main === module) {
    // 直接呼び出された時にだけ実行される処理
}

まとめ

Node.jsでファイルが直接実行されているかどうかは、require.main === moduleで判断することができる。   参考:https://nodejs.org/api/modules.html#modules_accessing_the_main_module

今回全部調べ終わってから気がついたのですが、「JavaScirpt 直接実行された場合のみ」とか検索すると、日本語記事もいくつかヒットしましたね。しかしながら、非推奨のmodule.parentを記載しているものもあったので、自分で調べてみてよかったです。