個人開発しているNext.jsのフルスタックアプリを用いて、VitestとReact Testing Libraryを使ったコンポーネントテストの方法を自習しました。
その中で、Vitestのモックを調べていたときにvi.hoisted()が出てきました。こちらが少し分かりにくかったので、備忘録としてまとめます。
vi.hoisted()とは
vi.hoisted()は、vi.mock()の中で使いたい値を、vi.mock()より先に用意するためのものです。
特に、モックした関数をテスト側で確認したいときによく使います。
const { mockPush } = vi.hoisted(() => ({
mockPush: vi.fn(),
}))
vi.fn()とvi.mock()って何だっけ
vi.fn()
vi.fn()は、偽物の関数を作るためのものです。
後から返り値を決めたり、何回呼ばれたか、どんな引数で呼ばれたかを確認したりできます。
const saveUser = vi.fn()
saveUser.mockResolvedValue({ id: '1' }) // mockResolvedValue(): 「Promiseが成功したときの返り値」を設定する関数
await saveUser({ name: 'Taro' })
expect(saveUser).toHaveBeenCalledWith({ name: 'Taro' }) // toHaveBeenCalledWith(): 指定した引数で関数が呼ばれたかを確認する関数
vi.mock()
vi.mock()は、モジュールごと偽物に差し替えるためのものです。vi.fn()が関数単位のモックなのに対して、vi.mock()はファイルからimportしている関数や値をまとめて差し替えられます。
この例では、@/authから読み込むauth、signIn、signOutをすべてvi.fn()に置き換えています。
そのため、テスト中にこれらの関数が呼ばれても、本来の認証処理は実行されません。
vi.mock("@/auth", async () => {
return {
auth: vi.fn(),
signIn: vi.fn(),
signOut: vi.fn(),
};
});
vi.mock()は見た目より先に実行される
ここがvi.hoisted()を使う理由です。
Vitestでは、vi.mock()はファイルの先頭へ移動して実行されます。つまり、テストコードでは他の値を先に書いても、先にvi.mock()が実行されます。
たとえば、次のように書くとします。
const mockPush = vi.fn()
vi.mock('next/navigation', () => ({
useRouter: () => ({
push: mockPush,
}),
}))
mockPushを先に書いているので問題なさそうに見えます。しかしvi.mock()が先に実行されるため、モックの生成時点ではmockPushがまだ使えず、エラーになります。
vi.hoisted()で先にモック関数を作る
そこで、vi.mock()の中で使うvi.fn()をvi.hoisted()で作ります。
const { mockAuth } = vi.hoisted(() => ({
mockAuth: vi.fn(),
}))
vi.mock('@/auth', () => ({
auth: mockAuth,
}))
mockAuth.mockResolvedValue({ user: { name: 'Taro' } })
こうすることで、
vi.hoisted()で作ったmockAuthは、vi.mock()より先に用意されます。そのため、auth部分の関数と、テストで返り値を決める関数に、同じmockAuthを使えます。
まとめ
英語でhoistは、「上に吊り上げる」という意味のようです。
Vitestではvi.mock()自体も上に移動して実行されます。vi.hoisted()は、vi.mock()より前に実行されるように上へ持っていくためのもの、と考えると覚えやすかったです。
テストを書くときにおそらく忘れるので、またこの記事に戻って思い出せるように書きました。
参考:
Vitest テストAPIリファレンス
フロントエンド単体テスト入門 [vitestハンズオン]
フロントエンドテスト入門 Part 2 — Reactコンポーネントを実際にテストしてみよう