🐌ESMのmock巻き上げ問題とVitestのvi.hoistedについてhosaka3132023/11/06に公開JavaScriptTypeScriptTestJestVitesttechはじめにVitestにはvi.hoistedというメソッドが用意されています。追加は2023年5月のv.0.31.0で、まだ日の浅いメソッドです。モジュール)でのmockのhoist(巻き上げ)、VitestのESM対応、vi.hoistedの挙動について調べてみました。モジュール)、CommonJSの基本知識がある下記の記事など参考になる記事がたくさんありますのでご覧ください。Vitestの基本知識があるインストール、mockの扱いなどについては記述を割きません。ESMでテストを書いている/書こうとしているVitestを使う場合はほぼ当てはまるかと思います。importについて簡単に整理します。import('../src/sample')両者にはさまざまな違いがありますが、今回問題になるのは評価(evaluate)順です。importの項には次のようにありました。読み込まれた時点で評価される結果となります。実際に動作検証している記事がわかりやすかったので挙げておきます。importは評価されるタイミングを指定できる、という違いがあるようです。importと同じです。(ただし、前者は同期、後者は非同期という違いあり。import(require)の順で評価される必要があります。そのために、Jest、Vitestでは.mockをファイル上部に巻き上げます。importの順が守られません。これはJestのドキュメントですが、下記の記述があります。code.(jest.mockの呼び出しは、ECMAScriptモジュールのサポートを有効にした場合、モジュールの最上部に巻き上げることはできません。ESMモジュールローダーは、コード実行前に常に静的インポートを評価されるためです。modules.(ESMはコードを見る前に静的なimport文を評価するため、CJSで発生するjest.mockの呼び出しの巻き上げはESMでは機能しません。モックされたモジュールを読み込むモジュールにも同じことが適用されます。)https://jestjs.io/ja/docs/ecmascript-modules#esm-におけるモジュールのモック「jest.mockの呼び出しの後にrequireまたは動的import()を使用してモックされたモジュールを読み込む必要があります」とあるのは、前項で見たとおり、これらは評価順をコントロールできるためですね。以下、Vitestに絞ってESMでのmockの評価順を調査してみました。importサンプルコードをいじりながら、Vitestの実際の挙動を見てみます。mockする関数として下記を用意しました。importを使ったテストコードを実行してみます。expect(sample()).toBe("これはmockです")})これはエラーになります。initializationとある通り、mockSampleより先にvi.mockが巻き上げられ実行されているようです。Vitestがどのように巻き上げをしているのか、該当しそうなVitestのソースコードにログを仕込んで確認してみました。hoistを行うソースコードhttps://github.com/vitest-dev/vitest/blob/4c957a27234cddda272d66c37300b8fa90370b6f/packages/vitest/src/node/hoistMocks.ts#L107-L193すると、下記のように変換されていました。expect(sample()).toBe("これはmockです");});ソースコードと見合わせながら確認すると、Vitestは次のようにコードを組み替えているようです。vi.unmockそのほかのimportしたがって、mockSampleの宣言前にmockが行われるため、未定義の変数参照となります。importの評価順を実現していることがわかりました。Vitestでは、importの変換とmock巻き上げでESM対応しているようです。imports.と書かれていることです。(厳密には"vitest"のimportよりは後...?)vi.hoistedしかし、mockの前にコードを実行したい場合に困ります。下記のようにmockの前に変数定義を差し込めないでしょうか。importに変換される)ここでようやくvi.hoistedの出番です。https://vitest.dev/api/vi.html#vi-hoisted実際にvi.hoistedを使ったサンプルで検証してみました。expect(sample()).toBe("これはmockです")})先ほどと同様に変換後のコードをみると、以下のようになっていました。expect(sample()).toBe("これはmockです");});vi.hoistedは引数のfactory関数の返り値を返しますので、今回はMock関数であるmockSampleをvi.mockの前に定義できていることになります。テスト実行もpassします。これができると、下記のようにmockSampleをtest内で操作しやすくなるので便利です。追加})まとめると、Vitestでは次の順で評価されることがわかりました。importに変換される)おわりに以上、vi.hoistedの立ち位置を理解するために、ESMでのmockとimportの実行順の問題、Vitestのコード実行順を調査しました。注意点として、vi.hoistedはまだ新しいメソッドなので、今後挙動が変わったりする可能性もあります。importへの変換の際のバグも報告されていました。(Destructuringで変換されるのでgetterが失われる。)https://github.com/vitest-dev/vitest/issues/3300#issuecomment-1534149023mock内で定義済みのMockを参照したい場合などに、参考にしていただければ幸いです。今回のサンプルコードは下記で公開しています。Issuehttps://github.com/vitest-dev/vitest/issues/3228テストコードvi.hoistedの仕様(spec)がわかります。本稿では扱いませんが、JestではESM対応として、jest.unstable_mockModuleを実験的に用意しているようです。より固定ピティナではエンジニアを募集中です。AWShttps://recruit.piano.or.jp/entry.htmlhosaka313いろいろ作って遊んでいます。バッジもらってみたい。全日本ピアノ指導者協会(ピティナ)Publicationバッジを贈って著者を応援しようバッジを受け取った著者にはZennから現金やAmazonギフトカードが還元されます。バッジを贈るDiscussionhosaka313いろいろ作って遊んでいます。バッジもらってみたい。Proよくある質問LinksX(Twitter)GitHubメディアキットLegal利用規約プライバシーポリシ。