[Jest]TypeError: Cannot read property 'readFileSync' of null


Overview

Jestでテストを実行した際、以下のエラーに遭遇しましたか?
もしあなたがesmのパッケージを利用しているなら、私はその問題の緩和策を提示できるでしょう。

TypeError: Cannot read property 'readFileSync' of null

      at fetch (node_modules/protobufjs/src/root.js:160:34)
      at Root.load (node_modules/protobufjs/src/root.js:194:13)
      at Root.loadSync (node_modules/protobufjs/src/root.js:235:17)
      at Object.loadSync (node_modules/protobufjs/src/index-light.js:69:17)
      at Object.<anonymous> (node_modules/@grpc/proto-loader/build/src/index.js:235:37)

Target reader

  • 前述のエラーに遭遇した方。

Prerequisite

  • Node.js v10.19.0
  • esmパッケージを利用している

Body

Jestとesmの関係について少しだけ言及

Jestはimport/exportの形式に執筆時点では対応していない。
つまりimport/exportの記述を可能にするesmとは相性が悪い。
その辺について知りたい場合は別記事を参照してください。
https://qiita.com/qrusadorz/items/f637953c0ada73483700#esm%E3%82%92%E5%B0%8E%E5%85%A5%E3%81%AB%E9%96%A2%E3%81%99%E3%82%8B%E6%B3%A8%E6%84%8F%E7%82%B9

esmをnodeコマンドで実行したり、サーバーレスのFunctionsで実行する分には問題ない。
それは私自身esmを実際に導入を完了しており、事実に基づいた発言である。
しかし、Jestだけはどうしてもだめだった

一応今回の問題はissueには上がっている。
https://github.com/standard-things/esm/issues/779

微妙に違うように見えるかもしれないが根本原因は一緒だと考えている。
恐らくこの問題は当分の間は解決されないだろうから、緩和策を模索した。

import文をあきらめる

人間引き際の判断が大事。幸いesmはrequireを混在して使ってもOKなので、エラーの発生する該当ソースはrequireのままにしておく。
本番ならそれだけでOKだが、Jestはesmの先にいるエラーが発生するパッケージのrequireを許してくれない
想像になるが、各テストのソースコード冒頭に置くconst requireEsm = require('esm')(module);
がrequireを置き換え、Jest環境下ではうまく行かないのだろう。

この考えのもと回避処理を作るとうまく行く。
まずはエラーが発生するテストコードの一行目を次のようにする。

hoge.test.js
global.requireJest = require; // ★esmが置き換える前のもとのrequireを保存しておく

// 通常はここが1行目で、esmのrequireを使用するだけでOK。
const requireEsm = require('esm')(module);
const { objectToArray } = requireEsm('../../utils/object')

ネーミングは重複しないなら何でもいいが、ここではrequireJestとして本来のrequireをglobalに保存する。
次にエラーが発生するimport部分に対応を入れる。
実際にエラーが発生したfirebase-adminのパッケージに対応した例を掲載する。

firebaseAdmin.js
// import admin from 'firebase-admin'; // 残念ながらJestではエラーになる
const admin = global.requireJest ? requireJest('firebase-admin') : require('firebase-admin'); // for jest test

前述でglobal.requireJestに保存したrequireがあるならそちらを利用し、ないなら本番環境なので通常のrequireを使用する。
これで本番でもJestでもどちらでも動くことが可能。

Conclusion

esmによりimport/exportが使用可能になり便利だが、最後にJestの落とし穴がある。
その箇所だけimportをあきらめれば完遂できるので、あきらめちゃだめだという話でした。

ちなみに私が一気に移行した際、firebaseAdmin.jsを参照しているテストが全てダメになった。
スタックトレースからはfirebaseAdmin.jsで発生したというのが不明のため、判明するのに時間かかりました。
(ちまちまスタブに置き換えてimport文一つ一つ探した

Have a great day!