使用 Vitest 测试 Svelte 5 组件
用 Vitest 测试 Svelte 5 组件,涵盖 mount、testing-library 和 browser mode,并说明 runes、effects 与 snippets 的正确配置。
Svelte 5 改变了组件测试的配置方式:你不再需要通过 new Component({ target }) 来实例化组件——该构造函数 API 已被移除。现在,你需要使用 svelte 中的 mount()、@testing-library/svelte 中的 render(),或 vitest-browser-svelte 中的 render() 来挂载组件。Rune($state、$derived、$effect、$props)只有在 Svelte 编译器处理文件后才会运行,因此你的测试配置必须将测试文件路由到该编译器进行处理。本指南涵盖当前正确的 Vitest 配置,以及在 Svelte 5 中测试 rune 和组件的具体模式。
核心要点
- 在 Svelte 5 中,
new Component({ target })构造函数以及$set/$on/$destroy已被移除;请使用svelte中的mount()或render()辅助函数来挂载组件,并通过$props()读取 props。 - 若要直接测试 rune,请将其放在文件名包含
.svelte的文件中(例如counter.svelte.test.ts),以便编译器在 Vitest 运行断言之前处理这些 rune。 - Effect 不会同步运行——请将使用
$effect的代码包裹在$effect.root()中,并调用flushSync()来刷新待处理的 effect,然后再执行断言。 - 在 Svelte 5 上使用
@testing-library/svelte时,需添加来自@testing-library/svelte/vite的svelteTesting插件;该插件会设置浏览器解析条件,并在每次测试后自动清理 DOM。 vitest-browser-svelte通过 Playwright 在真实浏览器中运行组件,需要 Vitest 4;始终使用await render(...),通过定位器查询元素,并使用await expect.element(...)进行断言。
Svelte 5 的测试有哪些变化?
网上大多数 Svelte 测试教程都是 Svelte 4 时代的,使用的是已不再存在的 API。如果某篇指南使用 new 实例化组件、调用 component.$set,或读取 $$props,那它已经过时了。以下是迁移对照表:
| Svelte 4(已移除) | Svelte 5(当前) |
|---|---|
new Component({ target }) | mount(Component, { target }) 或 render(Component) |
component.$set(props) | 将 props 传入 render / rerender |
component.$on / component.$destroy | 回调 props / unmount(component) |
$$props | $props() |
优先使用 fireEvent | 使用 userEvent 或浏览器模式定位器 |
svelte-jester 配置 | svelteTesting 插件(Vitest) |
目前有两种有效的配置方案。第一种是在 jsdom 上运行 @testing-library/svelte——层次更高、更为熟悉,且支持 Svelte 3、4 和 5 版本。第二种是 vitest-browser-svelte,它通过 Playwright 使用 Vitest 稳定版浏览器模式在真实浏览器中渲染组件。浏览器模式在 Vitest 4 中已正式发布,不再处于实验阶段,因此请忽略任何仍将其称为实验性功能的教程。
如何为 Svelte 测试配置 Vitest?
Discover how at OpenReplay.com.
所有 Svelte 5 测试配置都有一个共同要求:即使 Vitest 在 Node 环境中运行,也必须解析包的 browser 入口点。Svelte 文档通过 resolve.conditions 来实现这一点。从基础配置开始,逐步扩展。
对于在 jsdom 上运行的组件测试,安装 jsdom 并添加环境配置及 svelteTesting 插件:
// vite.config.js
import { defineConfig } from 'vitest/config';
import { svelte } from '@sveltejs/vite-plugin-svelte';
import { svelteTesting } from '@testing-library/svelte/vite';
export default defineConfig({
plugins: [svelte(), svelteTesting()],
test: {
environment: 'jsdom'
}
});
svelteTesting 插件会自动为你设置浏览器解析条件,并在 Vitest 中自动在每次测试前后完成 DOM 的初始化和清理——因此无需手动编写 afterEach(cleanup)。不要再手动配置 resolve.conditions,插件已经处理好了。
对于真实浏览器测试,请切换到 Vitest 的浏览器模式。从 Vitest 4 开始,provider 包需要单独安装,配置中从 @vitest/browser-playwright 导入 playwright(),并使用 instances 数组——旧的 provider: 'playwright', name: 'chromium' 写法已被废弃:
// vite.config.js
import { defineConfig } from 'vitest/config';
import { svelte } from '@sveltejs/vite-plugin-svelte';
import { playwright } from '@vitest/browser-playwright';
export default defineConfig({
plugins: [svelte()],
test: {
browser: {
enabled: true,
provider: playwright(),
instances: [{ browser: 'chromium' }]
}
}
});
编写组件测试
Svelte 5 组件通过 $props() 读取输入,并通过 $state 维护本地状态。以下是两种方案都将用到的测试组件:
<!-- Counter.svelte -->
<script>
let { initial = 0 } = $props();
let count = $state(initial);
</script>
<button onclick={() => count++}>{count}</button>
使用 @testing-library/svelte 时,调用 render,通过角色查询元素,使用 userEvent 驱动交互,并 await 点击操作:
import { render, screen } from '@testing-library/svelte';
import userEvent from '@testing-library/user-event';
import { expect, test } from 'vitest';
import Counter from './Counter.svelte';
test('increments on click', async () => {
const user = userEvent.setup();
render(Counter, { initial: 0 });
const button = screen.getByRole('button');
expect(button).toHaveTextContent('0');
await user.click(button);
expect(button).toHaveTextContent('1');
});
Svelte 自带的 mount()/unmount() 是这些辅助函数底层使用的低级 API。文档指出,原始的 mount() 方式”层次较低且有些脆弱”,因为它需要对精确的 innerHTML 进行断言,因此在组件测试中建议优先使用渲染辅助函数。
使用 vitest-browser-svelte 时,始终使用 await render(...),通过定位器查询元素,并使用 expect.element 进行断言,该方法会自动重试直到断言通过:
import { render } from 'vitest-browser-svelte';
import { expect, test } from 'vitest';
import Counter from './Counter.svelte';
test('increments on click', async () => {
const screen = await render(Counter, { initial: 0 });
const button = screen.getByRole('button');
await button.click();
await expect.element(button).toHaveTextContent('1');
});
测试 rune 和响应式逻辑
在挂载任何组件之前,先思考是否真的需要进行组件测试。Svelte 文档建议将响应式逻辑提取到 .svelte.js 模块中,并在不依赖组件开销的情况下对其进行隔离测试。该模块可以使用 rune,因为其文件名包含 .svelte:
// counter.svelte.js
export function createCounter(initial = 0) {
let count = $state(initial);
const doubled = $derived(count * 2);
return {
get count() { return count; },
get doubled() { return doubled; },
increment() { count++; }
};
}
直接对其进行测试——测试文件本身的名称也必须包含 .svelte,例如 counter.svelte.test.js,以便编译器在 Vitest 运行断言之前处理这些 rune:
import { expect, test } from 'vitest';
import { createCounter } from './counter.svelte.js';
test('derives doubled from count', () => {
const counter = createCounter(2);
expect(counter.doubled).toBe(4);
counter.increment();
expect(counter.doubled).toBe(6);
});
Effect 是个例外:它们不会同步运行。当被测代码使用 $effect 时,将其包裹在 $effect.root() 中,并调用 flushSync() 来在断言前执行待处理的 effect,这与 Svelte 测试文档中的示例完全一致:
import { flushSync } from 'svelte';
import { expect, test } from 'vitest';
import { logger } from './logger.svelte.js';
test('logs each update', () => {
const cleanup = $effect.root(() => {
let count = $state(0);
const log = logger(() => count);
flushSync();
expect(log).toEqual([0]);
count = 1;
flushSync();
expect(log).toEqual([0, 1]);
});
cleanup();
});
测试 snippet 和 props
Snippet 是 Svelte 5 对插槽(slot)的替代方案,通过 {@render} 渲染,并通过 $props() 接收。对于渲染 children snippet 的组件,最简单的测试方式是编写一个带有 data-testid 的小型包装组件,然后通过它进行查询。对于需要检查参数的 snippet,vitest-browser-svelte 文档使用 Svelte 的 createRawSnippet API 直接传入 snippet,并检查其接收到的内容:
<!-- Greeting.svelte -->
<script>
let { name, message } = $props();
const greeting = $derived(`Hello, ${name}!`);
</script>
<p>{@render message?.(greeting)}</p>
import { render } from 'vitest-browser-svelte';
import { createRawSnippet } from 'svelte';
import { expect, test } from 'vitest';
import Greeting from './Greeting.svelte';
test('passes the greeting into the snippet', async () => {
const screen = await render(Greeting, {
name: 'Alice',
message: createRawSnippet((greeting) => ({
render: () => `<span data-testid="message">${greeting()}</span>`
}))
});
await expect.element(screen.getByTestId('message'))
.toHaveTextContent('Hello, Alice!');
});
jsdom 与浏览器模式:如何选择
对于标记和逻辑的快速无浏览器测试,选择 jsdom + @testing-library/svelte;当需要真实的浏览器 API(如布局、焦点、IntersectionObserver)而不想进行 mock 时,选择 vitest-browser-svelte。
| jsdom + testing-library | vitest-browser-svelte | |
|---|---|---|
| 运行环境 | 模拟 DOM(jsdom) | 通过 Playwright 运行的真实浏览器 |
| 速度与配置 | 快速,无需下载浏览器 | 每次测试开销更大;需要浏览器 |
| 浏览器 API | 模拟实现 / mock | 原生支持,无需 mock |
| 同步刷新 | 通常需要 flushSync | 定位器自动重试;很少需要 |
| 依赖要求 | 支持 Svelte 3/4/5 | 需要 Vitest 4 |
由于浏览器模式的定位器和 expect.element 会自动重试直到断言成功,在该模式的组件测试中几乎不需要使用 flushSync——尽管少数边缘情况仍然需要。将纯响应式逻辑保留在 jsdom 的 .svelte.test 文件中以提升速度,将浏览器模式留给真正依赖真实渲染引擎的行为测试。
建议从将逻辑提取到 .svelte.js 模块并进行隔离测试开始,通过 svelteTesting 插件添加 jsdom 组件测试,当测试确实需要真实浏览器时再使用 vitest-browser-svelte。一次性完成配置,锁定上述当前 API,你的 Svelte 5 测试套件就能彻底摆脱那些导致大多数旧教程失效的已移除 Svelte 4 模式。
常见问题
为什么我的 $effect 在 Vitest 测试中没有运行?
Effect 在测试中不会同步运行,因此紧接在状态变更后执行的断言会读取到旧值。请将使用 effect 的代码包裹在 $effect.root() 中,并从 svelte 导入 flushSync() 来刷新待处理的 effect,然后再执行断言。测试结束时,调用 $effect.root() 返回的清理函数。在浏览器模式测试中,由于定位器和 expect.element 会自动重试,通常不需要这样做,但少数边缘情况仍需使用 flushSync。
测试 Svelte 5 组件还需要 svelte-jester 吗?
不需要——svelte-jester 是仅适用于 Jest 的方案,在 Vitest 中无需使用。对于 Vitest,请添加来自 @testing-library/svelte/vite 的 svelteTesting 插件,它会设置浏览器解析条件并在每次测试后自动清理 DOM。svelte-jester 在 testing-library 的配置文档中仍作为 Jest 的备选方案出现,但如果你使用的是 Vitest,应忽略它。许多旧教程照搬了 Jest 的配置路径,从而导致不必要的配置失败。
我可以在普通的 .test.js 文件中测试 Svelte 5 rune 吗?
不可以。Rune 只有在 Svelte 编译器处理文件后才会运行,而编译器只处理文件名包含 .svelte 的文件。若要直接测试 rune,请在文件名中包含 .svelte,例如 counter.svelte.test.js,这样编译器会在 Vitest 运行断言之前对 rune 进行转换。同样的规则适用于使用 rune 的普通模块:将其命名为包含 .svelte 的名称,例如 counter.svelte.js,然后在测试中正常导入即可。
vitest-browser-svelte 需要哪个版本的 Vitest?
vitest-browser-svelte 需要 Vitest 4.0.0 或更高版本;在 Vitest 3 或更早版本上安装会失败。浏览器模式在 Vitest 4 中正式稳定,该版本同时将 provider 包拆分为独立安装——你需要从 @vitest/browser-playwright 导入 playwright() 并配置 instances 数组。Vitest 2 中的旧写法 provider: 'playwright', name: 'chromium' 已被废弃,不再适用。该包托管在 GitHub 上的 vitest-community 组织下。
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k