vitestで関数内で呼んでいる同じファイル内の関数をmockする

vitestでテストを書いていて、mock化がうまくされないケースに遭遇した。
mock化自体は他の場所でもやっていたので、書き方は間違ってないはずなのだがなぜだかmock化されない。

動作を細かく調べてみると、ある関数内で呼び出している別の関数が同じファイル内にある場合だけmock化がされなかった。
何が起こっているかという現象と対策を調べたのでまとめておく。

原因を調査して一旦納得したものの、同じことをvitestではなくjestでやると問題なく動作することがわかった。
どうやらjestとvitestの問題というよりはESMかどうかでファイルの読み込み方が違うことによってmock時の挙動が変わるという方が正しそう?
この辺は分かり次第追記する。

目次

実際に起きたこと

まず実際に起こったことを簡略化したコードを使って説明する。
module.tsなるファイルがあったとして、foobarという2つの関数があり、barの中でfooを呼び出している。

export const fooFn = (str: string) => {
  return str
}

export const barFn = () => {
  return fooFn("foo")
}

この時barFn関数をテストしたいとする。
ただし、fooFn関数の結果はmock化したものを使いたいとする。
ということでこう書いてみた。

import { vi } from "vitest"
import * as moduleFns from "./module"

test('bar', () => {
    vi.spyOn(moduleFns, 'fooFn').mockReturnValue("mockedFoo")
    const result = moduleFns.barFn()

    expect(result).toBe("mockedFoo")
  })

spyOnを使ってfooFn関数をmock化したのでmockedFooが返ってくることを期待したが、実際にはfooが返ってきた。

原因の分析

最初何が起こったのかわからず、まずmock化できているかどうかを調べてみた。

import { vi } from "vitest"
import * as moduleFns from "./module"

test('bar', () => {
    vi.spyOn(moduleFns, 'fooFn').mockReturnValue("mockedFoo")
    console.log(moduleFns.fooFn("foo")) // mockedFoo
    const result = moduleFns.barFn()

    expect(result).toBe("mockedFoo")
})

mock化した後にconsole.logで出力してみるとちゃんとmockedFooが返ってきている。
つまりmock化はできているのにbarFnから呼び出した時だけmock化された結果が使われていない。

別のファイルの関数をmock化した時は意図した通りにmock化できていたことを考えると、同じファイルの時のみmock化されたものが使われていないということになる。

ここまで考えたところで参照が違うということかなと思って調べてみたらやっぱりそういうことらしかった。

Stack Overflow
How to mock functions in the same module using Jest? What's the best way to correctly mock the following example? The problem is that after import time, foo keeps the reference to the original unmocked bar. module...

対応策

参照が違うというのが原因なので参照が同じになる様に修正する必要がある。

調べた感じだとexportsを使う方法と、自分自身をimportする方法の2つっぽい。

// 案1
export const barFn = () => {
  return exports.fooFn("foo")
}

// 案2
import * as self from "./module"

export const barFn = () => {
  return self.fooFn("foo")
}

exportsmodule.exportsと同じもの。

どちらがいいのかということだが、型安全性を考慮して案2を採用した。
案1だとexports.fooFn()の型がanyになってしまってた一方、案2だとちゃんと推論が適切に効いていた。
案2だと循環参照になってしまうのが気になるが、ES6がいい感じに処理してくれてるので特に問題はなさそうに思えたのでこちらを採用。

いずれにせよプロダクションコードをテストのために書き換えないといけないのがモヤるが、調べたところ解決策なさそうなので妥協する。
何かいい解決方法知ってる人いたら教えてください。

jestだとこの現象は起こらなかった。
おそらくESMのファイル読み込みが関連してそうだが、分かり次第追記する。

まとめ

不可解な現象に遭遇して最初意味がわからなかった。
色々調べる前にデバッグして原因を当たりをつけられたのはよかった。

この記事が参考になったからコーヒーくらいおごってもいいぜという方は、以下からサポートいただけると次の記事書くモチベになりますのでよろしくお願いします

Buy Me A Coffee

参考

Stack Overflow
How to mock functions in the same module using Jest? What's the best way to correctly mock the following example? The problem is that after import time, foo keeps the reference to the original unmocked bar. module...
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次