はじめに
Vitestでテストを作成中にエラーが発生したので、その解消方法を備忘録としてまとめます。
同じエラーに遭遇した方の参考になれば幸いです。
発生したエラーメッセージ
Error: [vitest] There was an error when mocking a module. If you are using "vi.mock" factory, make sure there are no top level variables inside, since this call is hoisted to top of the file. Read more: https://vitest.dev/api/vi.html#vi-mock
Caused by: ReferenceError: Cannot access 'supabaseMock' before initialization
エラー発生時のソースコード
import { render,screen } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";
import { Login } from "../pages/Login";
import userEvent from "@testing-library/user-event";
// useNavigateのモック化(一部モック化)
const mockedNavigator = vi.fn();
vi.mock("react-router-dom", async () => {
const actual = await vi.importActual<typeof import("react-router-dom")>("react-router-dom");
return {
...actual,
useNavigate: () => mockedNavigator,
};
});
// supabaseのモック化
const supabaseMock = {
auth: {
getSession: vi.fn(),
onAuthStateChange: vi.fn(() => ({
data: { subscription: { unsubscribe: vi.fn() } },
})),
signOut: vi.fn(),
},
}
vi.mock("../utils/supabase", () => ({
supabase: supabaseMock,
}));
// fetchUserのモック作成
const fetchUserMock = vi.fn();
vi.mock("../service/fetchUser", () => ({
fetchUser: fetchUserMock
}));
// テストで使うユーザー操作イベントを定義
const user = userEvent.setup();
describe("初期表示", () => {
// セッションの初期化
beforeEach(() => {
supabaseMock.auth.getSession.mockResolvedValue({
data: { session: null }, // ログインしていないデフォルト
});
});
test("ユーザーが Supabase の users テーブルに存在する場合、検索画面に遷移すること", async () => {
// ログインユーザとユーザを取得
supabaseMock.auth.getSession.mockResolvedValue({
data: { session: { user: { id: "user-123" } } },
});
fetchUserMock.mockResolvedValue({ id: "user-123" });
render(
<MemoryRouter>
<Login />
</MemoryRouter >
);
const loginButton = await screen.findByRole("button", { name: "Googleでログイン" })
await user.click(loginButton);
expect(mockedNavigator).toHaveBeenCalledWith("/substitute-search")
})
});
原因
Vitestでは vi.mock が ファイル冒頭に強制的に移動(ホイスティング) されます。
そのため、モック化に使う変数がまだ定義されていない状態で参照されてしまい、Cannot access before initialization が発生します。
処理の流れとしては以下のようになります。
1. Vitest がファイルを読み込む
2. vi.mock が強制的に先頭へ移動する(ホイスティング)
3. まだ定義されていない supabaseMock を参照してしまう
解決策
vi.hoisted を使用する
vi.hoisted はトップレベル変数を「安全に」定義するための仕組みです。
これを使うことで、vi.mock 内から参照する変数も正しく初期化されます。
// supabase,fetchUserのモック化
const { supabaseMock, fetchUserMock } = vi.hoisted(() => ({
supabaseMock: {
auth: {
getSession: vi.fn(),
onAuthStateChange: vi.fn(() => ({
data: { subscription: { unsubscribe: vi.fn() } },
})),
signOut: vi.fn(),
},
},
fetchUserMock: vi.fn(),
}));
vi.mock("../utils/supabase", () => ({
supabase: supabaseMock,
}));
vi.mock("../service/fetchUser", () => ({
fetchUser: fetchUserMock
}));
終わりに
今回のエラーは、vi.mock
が強制的にホイスティングされる仕組みを理解していなかったことが原因でした。
vi.hoisted
を使えばモック変数を安全に定義できるので、Vitestでモックを扱う際には覚えておきたいポイントです。
自分自身もまだVitestの理解が十分ではないので、引き続き学習を進めていきます。
参考