SvelteKit+Vitest单元测试:如何Mock $app/state的page对象
问题描述
首次在Stack Overflow发帖,刚接触SvelteKit。正在测试Svelte 5的Navbar组件,组件会根据page.data.user(判断用户是否登录)、page.url.pathname(在特定路由隐藏部分链接)显示不同的链接/按钮。组件在浏览器中运行正常,但无法在Vitest + Testing Library的单元测试中Mock page对象。
需要实现模拟page.data.user = { name: 'Alex' }、page.url.pathname = '/search'这类场景,技术栈:SvelteKit 2.17.2、Svelte 5.20.2、Vitest 3.0.0、@testing-library/svelte 5.2.7。
Navbar.svelte代码
<script lang="ts"> import { page } from '$app/state'; import { goto, invalidateAll } from '$app/navigation'; let user = $state(page.data.user); let currentPath = $state(page.url.pathname || ''); $effect(() => { user = page.data.user; currentPath = page.url.pathname || ''; invalidateAll(); }); async function handleLogout() { try { const formData = new FormData(); const response = await fetch('/logout', { method: 'POST', body: formData }); if (response.ok) { await goto('/'); } } catch (error) { console.error('Logout error:', error); } } </script> <nav class="fixed top-0 right-0 left-0 z-50 flex h-16 items-center border-b border-gray-200 bg-white px-6 font-serif" > <!-- Left --> <div class="flex-1"> {#if !user && !['/register', '/login', '/search'].includes(currentPath)} <a href="/register"> <button class="rounded border border-gray-300 px-4 py-2 text-gray-700 hover:bg-gray-50"> Register </button> </a> {/if} </div> <!-- Right --> <div class="flex flex-1 items-center justify-end"> {#if user} <button onclick={handleLogout} class="ml-4 rounded border border-gray-300 px-4 py-2 text-gray-700 hover:bg-gray-50" > Logout </button> {:else if !['/register', '/login', '/search'].includes(currentPath)} <a href="/login" class="text-gray-700 hover:text-gray-900">Sign in</a> {/if} </div> </nav>
初始测试代码
import { describe, it, expect, vi } from 'vitest'; import { render, screen } from '@testing-library/svelte'; import Navbar from './Navbar.svelte'; vi.mock('$app/navigation', () => ({ goto: vi.fn(), invalidateAll: vi.fn(), })); // 需在此处Mock $app/state.page describe('navigation bar', () => { it('shows logout button when user is logged in', async () => { render(Navbar); expect(screen.getByRole('button', { name: /logout/i })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: /register/i })).not.toBeInTheDocument(); expect(screen.queryByRole('link', { name: /sign in/i })).not.toBeInTheDocument(); }); });
解决方案
要Mock $app/state中的page对象,核心是模拟SvelteKit的page可读store结构,利用Vitest的vi.mock方法实现,同时支持动态修改测试状态。
修改后的完整测试代码
import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen } from '@testing-library/svelte'; import Navbar from './Navbar.svelte'; import { readable } from 'svelte/store'; // 定义可修改的全局mock状态,用于控制page的值 let mockPageState = { data: { user: null }, url: { pathname: '/' } }; // Mock $app/state模块,返回模拟的page store vi.mock('$app/state', () => ({ page: readable(mockPageState) })); vi.mock('$app/navigation', () => ({ goto: vi.fn(), invalidateAll: vi.fn(), })); // 每个测试用例前重置状态,避免测试间污染 beforeEach(() => { mockPageState = { data: { user: null }, url: { pathname: '/' } }; vi.clearAllMocks(); }); describe('navigation bar', () => { it('shows logout button when user is logged in', async () => { // 设置登录状态 mockPageState.data.user = { name: 'Alex' }; render(Navbar); expect(screen.getByRole('button', { name: /logout/i })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: /register/i })).not.toBeInTheDocument(); expect(screen.queryByRole('link', { name: /sign in/i })).not.toBeInTheDocument(); }); it('hides register and sign in links on /search path', async () => { // 设置当前路由为/search mockPageState.url.pathname = '/search'; render(Navbar); expect(screen.queryByRole('button', { name: /register/i })).not.toBeInTheDocument(); expect(screen.queryByRole('link', { name: /sign in/i })).not.toBeInTheDocument(); }); it('shows register and sign in when user is logged out on home path', async () => { render(Navbar); expect(screen.getByRole('button', { name: /register/i })).toBeInTheDocument(); expect(screen.getByRole('link', { name: /sign in/i })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: /logout/i })).not.toBeInTheDocument(); }); });
关键说明
- 模拟page store:用Svelte的
readable创建模拟的pagestore,初始值绑定到mockPageState,组件订阅时能获取到状态。 - 状态隔离:通过
beforeEach在每个测试前重置mockPageState和mock函数,避免测试用例间的状态污染。 - 动态修改状态:直接修改
mockPageState后重新渲染组件,组件的$effect会自动捕获新的page状态并更新UI。
另外,组件中$effect里的invalidateAll()调用可以去掉——这个方法是用来刷新服务器数据的,这里只是同步page状态,没必要保留,还能减少测试中的mock调用。
内容的提问来源于stack exchange,提问作者InternationalBonus16
相关产品推荐
相关产品推荐

