12k
All articles

使用 Vitest 测试 Svelte 5 组件

用 Vitest 测试 Svelte 5 组件,涵盖 mount、testing-library 和 browser mode,并说明 runes、effects 与 snippets 的正确配置。

OpenReplay Team
OpenReplay Team
使用 Vitest 测试 Svelte 5 组件

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/vitesvelteTesting 插件;该插件会设置浏览器解析条件,并在每次测试后自动清理 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?

所有 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-libraryvitest-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 组织下。

DevTools for the frontend

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

We use cookies to improve your experience. By using our site, you accept cookies.