vitestで関数内で呼んでいる同じファイル内の関数をmockする
vitestでテストを書いていて、mock化がうまくされないケースに遭遇した。
mock化自体は他の場所でもやっていたので、書き方は間違ってないはずなのだがなぜだかmock化されない。
動作を細かく調べてみると、ある関数内で呼び出している別の関数が同じファイル内にある場合だけmock化がされなかった。
何が起こっているかという現象と対策を調べたのでまとめておく。
原因を調査して一旦納得したものの、同じことをvitestではなくjestでやると問題なく動作することがわかった。
どうやらjestとvitestの問題というよりはESMかどうかでファイルの読み込み方が違うことによってmock時の挙動が変わるという方が正しそう?
この辺は分かり次第追記する。
実際に起きたこと
まず実際に起こったことを簡略化したコードを使って説明する。
module.ts
なるファイルがあったとして、foo
とbar
という2つの関数があり、bar
の中でfoo
を呼び出している。
この時barFn
関数をテストしたいとする。
ただし、fooFn
関数の結果はmock化したものを使いたいとする。
ということでこう書いてみた。
spyOn
を使ってfooFn
関数をmock化したのでmockedFoo
が返ってくることを期待したが、実際にはfoo
が返ってきた。
原因の分析
最初何が起こったのかわからず、まずmock化できているかどうかを調べてみた。
mock化した後にconsole.log
で出力してみるとちゃんとmockedFoo
が返ってきている。
つまりmock化はできているのにbarFn
から呼び出した時だけmock化された結果が使われていない。
別のファイルの関数をmock化した時は意図した通りにmock化できていたことを考えると、同じファイルの時のみmock化されたものが使われていないということになる。
ここまで考えたところで参照が違うということかなと思って調べてみたらやっぱりそういうことらしかった。
対応策
参照が違うというのが原因なので参照が同じになる様に修正する必要がある。
調べた感じだとexports
を使う方法と、自分自身をimportする方法の2つっぽい。
exports
はmodule.exports
と同じもの。
どちらがいいのかということだが、型安全性を考慮して案2を採用した。
案1だとexports.fooFn()
の型がanyになってしまってた一方、案2だとちゃんと推論が適切に効いていた。
案2だと循環参照になってしまうのが気になるが、ES6がいい感じに処理してくれてるので特に問題はなさそうに思えたのでこちらを採用。
いずれにせよプロダクションコードをテストのために書き換えないといけないのがモヤるが、調べたところ解決策なさそうなので妥協する。
何かいい解決方法知ってる人いたら教えてください。
jestだとこの現象は起こらなかった。
おそらくESMのファイル読み込みが関連してそうだが、分かり次第追記する。
まとめ
不可解な現象に遭遇して最初意味がわからなかった。
色々調べる前にデバッグして原因を当たりをつけられたのはよかった。