Svelte 5 测试实践指南:Vitest 单测与组件测试、Storybook 交互测试与 Playwright E2E
本文基于 Svelte 官方文档 Testing 整理并展开,覆盖 Svelte 项目的三层测试体系:使用 Vitest 编写单元测试与组件测试(包括在测试文件中直接使用 runes 的关键技巧)、使用 Storybook 做浏览器环境下的组件交互测试、使用 Playwright 做端到端测试,并结合 Svelte 仓库自身的 vitest.config.js 与 package.json 源码配置,解释文档中每项配置背后的真实原因,读完即可在自己项目中搭起一套完整的 Svelte 测试方案。
整体思路:Svelte 对测试框架保持中立
Svelte 官方对测试框架持开放态度:单元测试、集成测试、端到端测试都可以自由选择实现方案,官方文档中给出的示例组合是 Vitest(单元/组件测试)、Storybook(浏览器中的组件测试)、Playwright(端到端测试),同时兼容 Jasmine、Cypress、NightwatchJS 等替代方案。
这套分层策略的核心价值在于:
- 单元测试:验证
.svelte.js/.svelte.ts中导出的孤立逻辑,速度最快; - 组件测试:在真实或模拟的 DOM 中渲染组件,模拟交互并断言;
- E2E 测试:以用户视角走完整应用流程,测试代码完全不感知 Svelte 框架本身。
下面逐层展开,并给出与 Svelte 仓库自身测试基建相互印证的实现细节。
用 Vitest 搭建单元与组件测试环境
如果你使用 Vite(包括通过 SvelteKit),官方推荐 Vitest。可以用 Svelte CLI 在项目创建时或之后一键配置 Vitest;下面是手动配置的完整步骤。
安装与 vite.config.js 配置
第一步,安装 Vitest:
npm install -D vitest
第二步,调整 vite.config.js:
// file: vite.config.js
import { defineConfig } from 'vitest/config';
export default defineConfig({
// ...
// Tell Vitest to use the `browser` entry points in `package.json` files, even though it's running in Node
resolve: process.env.VITEST
? {
conditions: ['browser']
}
: undefined
});
注意:如果加载全部包的浏览器版本不合适(例如你同时要测试后端库),可能需要改用 alias 配置来按需指认入口。
为什么要加 conditions: ['browser']? 这一点在 Svelte 仓库自身的 packages/svelte/package.json 中可以得到直接印证。Svelte 包的 exports 字段对主入口按条件区分了浏览器与服务端两份实现:
".": {
"types": "./types/index.d.ts",
"worker": "./src/index-server.js",
"browser": "./src/index-client.js",
"default": "./src/index-server.js"
}
也就是说:当 Node 环境(Vitest 默认运行环境)解析 svelte 时,default 条件会命中 src/index-server.js(服务端渲染实现);而组件测试需要的是操作 DOM 的客户端实现 src/index-client.js。resolve.conditions: ['browser'] 正是告诉 Vitest「虽然是 Node 在跑,但请优先使用 browser 入口」。
Svelte 仓库的根 vitest.config.js 是这一思路的更精细版本:由于要在同一套测试里同时跑服务端与客户端代码,它没有简单加 browser 条件,而是用自定义 customResolver 按调用方路径动态选择入口——
// vitest.config.js(Svelte 仓库自身配置,节选)
alias: [
{
find: /^svelte\/?/,
customResolver: (id, importer) => {
const exported = pkg.exports[id === 'undefined' ? '.' : id.replace('undefined', './')];
if (!exported) return;
// When running the server version of the Svelte files,
// we also want to use the server export of the Svelte package
return path.resolve(
'packages/svelte',
importer?.includes('_output/server')
? exported.default
: exported.browser ?? exported.default
);
}
}
]
此外该配置还包含一些对大型测试工程有参考价值的设置:testTimeout: 30_000(因为部分 dev 模式测试会触发 effect_update_depth_exceeded 保护,单次 flush 会产生上千个 Error 对象做栈追踪,默认 5 秒不够用)、coverage.provider: 'v8' 覆盖采集、以及通过 include/exclude 精确圈定测试入口(packages/svelte/**/*.test.ts、packages/svelte/tests/*/test.ts 等)。
单元测试:直接测试 .svelte.js 中的逻辑
配置完成后,即可对 .js/.ts 文件中的代码写单元测试。官方示例是一个 multiplier 状态封装:
被测逻辑(multiplier.svelte.ts):
export function multiplier(initial: number, k: number) {
let count = $state(initial);
return {
get value() {
return count * k;
},
set: (c: number) => {
count = c;
}
};
}
测试文件(multiplier.svelte.test.js):
import { flushSync } from 'svelte';
import { expect, test } from 'vitest';
import { multiplier } from './multiplier.svelte.js';
test('Multiplier', () => {
let double = multiplier(0, 2);
expect(double.value).toEqual(0);
double.set(5);
expect(double.value).toEqual(10);
});
等价的 JSDoc 类型版本(multiplier.svelte.js)同样可用,Svelte 编译与 Vite 处理对 TypeScript 和 JSDoc 标注一视同仁:
/**
* @param {number} initial
* @param {number} k
*/
export function multiplier(initial, k) {
let count = $state(initial);
return {
get value() {
return count * k;
},
/** @param {number} c */
set: (c) => {
count = c;
}
};
}
在测试文件中使用 runes
这是 Svelte 5 测试与普通 JS 单元测试最大的差异点:Vitest 会通过 Svelte 的 Vite 插件以与源码文件相同的方式处理测试文件,因此只要测试文件名包含 .svelte,就可以在其中直接使用 runes——上面示例文件名写作 multiplier.svelte.test.js 正是这个原因。
例如被测逻辑不自己持有状态,而是接收一个取值函数,测试里就可以直接用 $state:
// multiplier.svelte.test.js
import { flushSync } from 'svelte';
import { expect, test } from 'vitest';
import { multiplier } from './multiplier.svelte.js';
test('Multiplier', () => {
let count = $state(0);
let double = multiplier(() => count, 2);
expect(double.value).toEqual(0);
count = 5;
expect(double.value).toEqual(10);
});
对应的被测代码:
/**
* @param {() => number} getCount
* @param {number} k
*/
export function multiplier(getCount, k) {
return {
get value() {
return getCount() * k;
}
};
}
涉及 $effect 时必须用 $effect.root 包裹:effects 不会在普通函数调用上下文里工作,需要根节点挂载。官方示例用 logger 演示了完整套路——$effect.root 建立根、flushSync 把本来在微任务后才执行的 effects 同步冲刷掉、最后调用返回的 cleanup 函数清理:
// logger.svelte.test.js
import { flushSync } from 'svelte';
import { expect, test } from 'vitest';
import { logger } from './logger.svelte.js';
test('Effect', () => {
const cleanup = $effect.root(() => {
let count = $state(0);
// logger uses an $effect to log updates of its input
let log = logger(() => count);
// effects normally run after a microtask,
// use flushSync to execute all pending effects synchronously
flushSync();
expect(log).toEqual([0]);
count = 1;
flushSync();
expect(log).toEqual([0, 1]);
});
cleanup();
});
被测的 logger:
/**
* @param {() => any} getValue
*/
export function logger(getValue) {
/** @type {any[]} */
let log = [];
$effect(() => {
log.push(getValue());
});
return log;
}
源码层面的对应关系:
$effect.root的内部实现在 packages/svelte/src/internal/client/reactivity/effects.js:effect_root先Batch.ensure()确保当前批次存在,再创建带ROOT_EFFECT标志的 effect 执行fn,并返回一个销毁该 effect 的清理函数——这正是测试中cleanup()的作用,防止跨测试串扰(批处理层还提供了clear()用于在测试间清空全部批次)。flushSync的实现在 packages/svelte/src/internal/client/reactivity/batch.js:它循环执行flush_tasks()并冲刷current_batch,直到不再有待处理的批次,从而保证调用返回时所有 effects/DOM 更新已同步完成,测试才能在同一次调用中写出确定性的断言。
组件测试:jsdom + mount
组件测试可以在真实或模拟的浏览器中渲染组件、模拟行为、做断言,而无需启动整个应用。
官方建议:写组件测试前先想清楚——你是真的要测组件本身,还是组件内部的逻辑?如果是后者,优先把逻辑抽出来单独测,省去组件测试的开销。
第一步,安装 jsdom(模拟 DOM API 的库):
npm install -D jsdom
第二步,在 vite.config.js 中配置 DOM 环境(注意同时保留上面的 browser conditions 设置):
// file: vite.config.js
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [
/* ... */
],
test: {
// If you are testing components client-side, you need to set up a DOM environment.
// If not all your files should have this environment, you can use a
// `// @vitest-environment jsdom` comment at the top of the test files instead.
environment: 'jsdom'
},
// Tell Vitest to use the `browser` entry points in `package.json` files, even though it's running in Node
resolve: process.env.VITEST
? {
conditions: ['browser']
}
: undefined
});
如果只有部分测试文件需要 jsdom 环境,可以不在全局配置 environment,改为在对应测试文件顶部写 // @vitest-environment jsdom 注释,粒度更细。
第三步,用 Svelte 的 mount API 实例化组件并断言。完整可运行示例:
// component.test.js
import { flushSync, mount, unmount } from 'svelte';
import { expect, test } from 'vitest';
import Component from './Component.svelte';
test('Component', () => {
// Instantiate the component using Svelte's `mount` API
const component = mount(Component, {
target: document.body, // `document` exists because of jsdom
props: { initial: 0 }
});
expect(document.body.innerHTML).toBe('<button>0</button>');
// Click the button, then flush the changes so you can synchronously write expectations
document.body.querySelector('button')?.click();
flushSync();
expect(document.body.innerHTML).toBe('<button>1</button>');
// Remove the component from the DOM
unmount(component);
});
这里的 mount/unmount/hydrate 均从 svelte 主入口导出,最终指向 packages/svelte/src/internal/client/render.js 中的 mount(第 66 行)等函数;这也是 Svelte 5 与 Svelte 4 的显著区别——组件不再是类,而是用 mount(component, options) 这样的函数式 API 挂载(packages/svelte/src/index-client.js 中可看到 export { hydrate, mount, unmount } from './internal/client/render.js')。
这种写法直白但偏底层且脆弱——组件内部结构一变,document.body.innerHTML 断言就容易碎。此时推荐改用 Testing Library(如 @testing-library/svelte),上面的测试可以重写为:
// component.test.js
import { render, screen } from '@testing-library/svelte';
import userEvent from '@testing-library/user-event';
import { expect, test } from 'vitest';
import Component from './Component.svelte';
test('Component', async () => {
const user = userEvent.setup();
render(Component);
const button = screen.getByRole('button');
expect(button).toHaveTextContent(0);
await user.click(button);
expect(button).toHaveTextContent(1);
});
Testing Library 按可访问性角色(getByRole)与文本定位元素,对组件内部实现细节不敏感。对于涉及双向绑定、context 或 snippet props 的组件测试,官方建议为具体测试单独创建一个包装组件,然后与包装组件交互,而不是直接硬造这些依赖。
用 Storybook 做浏览器中的组件测试
Storybook 既是 UI 组件开发与文档工具,也能做组件测试:它跑在 Vitest 的 browser mode 下,把组件渲染进真实浏览器,提供最贴近生产的环境。
接入方式:在项目中通过 npx sv add storybook 安装 Svelte 的 Storybook 集成,并选择带测试能力的推荐配置;若项目已有 Storybook,直接按官方测试文档推进即可。
核心是用 play 函数 模拟用户行为并做断言(借助 Testing Library 与 Vitest API)。示例为一个 LoginForm 的两个 story:一个渲染空表单,一个模拟用户填表提交:
<!-- file: LoginForm.stories.svelte -->
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import { expect, fn } from 'storybook/test';
import LoginForm from './LoginForm.svelte';
const { Story } = defineMeta({
component: LoginForm,
args: {
// Pass a mock function to the `onSubmit` prop
onSubmit: fn(),
}
});
</script>
<Story name="Empty Form" />
<Story
name="Filled Form"
play={async ({ args, canvas, userEvent }) => {
// Simulate a user filling out the form
await userEvent.type(canvas.getByTestId('email'), 'email@provider.com');
await userEvent.type(canvas.getByTestId('password'), 'a-random-password');
await userEvent.click(canvas.getByRole('button'));
// Run assertions
await expect(args.onSubmit).toHaveBeenCalledTimes(1);
await expect(canvas.getByText('You’re in!')).toBeInTheDocument();
}}
/>
要点:通过 args 给 onSubmit 传入 mock 函数(fn()),play 函数里既能用 canvas 定位并操作元素,又能用 expect(args.onSubmit).toHaveBeenCalledTimes(1) 这类断言验证回调行为,实现「交互 + 行为」一体的组件级验收测试。
用 Playwright 做端到端测试
E2E 测试以用户视角验证完整应用。官方示例以 Playwright 为例,也可换成 Cypress、NightwatchJS。
接入方式有两种:Svelte CLI 可以在创建项目时或之后一键配置 Playwright;或者直接用 npm init playwright 初始化。若用 IDE 开发,还可安装 Playwright 的 VS Code 扩展,在编辑器内直接执行测试。
如果通过 npm init playwright 初始化或项目不使用 Vite,可能需要调整 Playwright 配置,告诉它跑测试前先做什么——关键是用 webServer 在指定端口启动应用:
// file: playwright.config.js
const config = {
webServer: {
command: 'npm run build && npm run preview',
port: 4173
},
testDir: 'tests',
testMatch: /(.+\.)?(test|spec)\.[jt]s/
};
export default config;
配置项说明:
webServer.command:测试执行前的启动命令,这里选择先构建再预览(preview端口为 4173),验证的是产物而非开发服务器;webServer.port:与command中preview的端口保持一致;testDir:测试文件目录;testMatch:匹配*.test.js/ts与*.spec.js/ts文件。
E2E 测试完全不了解 Svelte 的存在,只与 DOM 打交道:
// file: tests/hello-world.spec.js
import { expect, test } from '@playwright/test';
test('home page has expected h1', async ({ page }) => {
await page.goto('/');
await expect(page.locator('h1')).toBeVisible();
});
Svelte 仓库自身如何验证这些流程
Svelte 仓库本身就完整示范了本文的全部工具链,可以作为参照:
- Vitest 版本与 E2E 依赖:packages/svelte/package.json 的
devDependencies中同时包含vitest(^4.1.7)与@playwright/test(^1.62.0),与文档推荐一致; - 测试组织方式:测试入口集中在
packages/svelte/tests/下的多个套件(runtime-runes、hydration、server-side-rendering、compiler-errors等,各套件一个test.ts),由根 vitest.config.js 的include统一收集,packages/svelte/**/*.test.ts则覆盖源码旁的单元测试(如 proxy.test.ts、clone.test.ts); - browser 条件的真实工程化:如前文所述,Svelte 仓库没有简单全局开启
browser条件,而是用customResolver按调用者区分客户端/服务端入口——如果你的项目也需要同时测试前端组件与服务端逻辑,这个 vitest.config.js 值得直接参考。
小结
| 测试层级 | 推荐工具 | 关键点 |
|---|---|---|
| 单元测试 | Vitest | 测试 .svelte.js 导出逻辑;文件名含 .svelte 才能在测试中用 runes |
| 含 effects 的逻辑 | Vitest + $effect.root |
用 flushSync 同步冲刷,结束后调用返回的 cleanup |
| 组件测试 | Vitest + jsdom / Testing Library | vite.config.js 加 environment: 'jsdom' 与 resolve.conditions: ['browser'],用 mount/unmount 挂载卸载 |
| 组件交互验收 | Storybook + Vitest browser mode | play 函数模拟用户行为并断言 |
| E2E | Playwright | webServer 在固定端口启动应用,测试代码只与 DOM 交互 |
遵循「逻辑下沉可单测、组件用 mount 或 Testing Library、全流程交给 Playwright」的分层原则,即可为 Svelte 5 项目建立起覆盖全面、且每一层都有官方文档与仓库自身实践背书的测试体系。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00