测试
测试可以帮助您编写和维护代码,防止出现回归问题。测试框架可以帮助您完成这一目标,通过让您能够描述对代码行为的断言或期望。
Svelte 对于您使用哪种测试框架并不持有特定立场 — 您可以使用 Vitest、Jasmine、Cypress 和 Playwright 等解决方案来编写单元测试、集成测试和端到端测试。
使用 Vitest 进行单元和集成测试
单元测试允许您测试代码中的小型独立部分。集成测试允许您测试应用程序的各个部分,以查看它们是否协同工作。如果您使用 Vite(包括通过 SvelteKit),我们推荐使用 Vitest。
首先安装 Vitest:
npm install -D vitest
然后调整您的 vite.config.js
:
import { function defineConfig(config: UserConfig): UserConfig (+3 overloads)
defineConfig } from 'vitest/config';
export default function defineConfig(config: UserConfig): UserConfig (+3 overloads)
defineConfig({
// ...
// 告诉 Vitest 使用 `package.json` 文件中的 `browser` 入口点,即使它在 Node 中运行
UserConfig.resolve?: (ResolveOptions & {
alias?: AliasOptions;
}) | undefined
Configure resolver
resolve: var process: NodeJS.Process
process.NodeJS.Process.env: NodeJS.ProcessEnv
The process.env
property returns an object containing the user environment.
See environ(7)
.
An example of this object looks like:
{
TERM: 'xterm-256color',
SHELL: '/usr/local/bin/bash',
USER: 'maciej',
PATH: '~/.bin/:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin',
PWD: '/Users/maciej',
EDITOR: 'vim',
SHLVL: '1',
HOME: '/Users/maciej',
LOGNAME: 'maciej',
_: '/usr/local/bin/node'
}
It is possible to modify this object, but such modifications will not be
reflected outside the Node.js process, or (unless explicitly requested)
to other Worker
threads.
In other words, the following example would not work:
node -e 'process.env.foo = "bar"' && echo $foo
While the following will:
import { env } from 'node:process';
env.foo = 'bar';
console.log(env.foo);
Assigning a property on process.env
will implicitly convert the value
to a string. This behavior is deprecated. Future versions of Node.js may
throw an error when the value is not a string, number, or boolean.
import { env } from 'node:process';
env.test = null;
console.log(env.test);
// => 'null'
env.test = undefined;
console.log(env.test);
// => 'undefined'
Use delete
to delete a property from process.env
.
import { env } from 'node:process';
env.TEST = 1;
delete env.TEST;
console.log(env.TEST);
// => undefined
On Windows operating systems, environment variables are case-insensitive.
import { env } from 'node:process';
env.TEST = 1;
console.log(env.test);
// => 1
Unless explicitly specified when creating a Worker
instance,
each Worker
thread has its own copy of process.env
, based on its
parent thread’s process.env
, or whatever was specified as the env
option
to the Worker
constructor. Changes to process.env
will not be visible
across Worker
threads, and only the main thread can make changes that
are visible to the operating system or to native add-ons. On Windows, a copy of process.env
on a Worker
instance operates in a case-sensitive manner
unlike the main thread.
env.string | undefined
VITEST
? {
ResolveOptions.conditions?: string[] | undefined
conditions: ['browser']
}
: var undefined
undefined
});
如果加载所有包的浏览器版本不适合您的需求,例如您还需要测试后端库,您可能需要使用别名配置
现在您可以为 .js/.ts
文件中的代码编写单元测试:
import { function flushSync(fn?: (() => void) | undefined): void
Synchronously flushes any pending state changes and those that result from it.
flushSync } from 'svelte';
import { const expect: ExpectStatic
expect, const test: TestAPI
Defines a test case with a given name and test function. The test function can optionally be configured with test options.
test } from 'vitest';
import { import multiplier
multiplier } from './multiplier.js';
test<object>(name: string | Function, fn?: TestFunction<object> | undefined, options?: number | TestOptions): void (+2 overloads)
Defines a test case with a given name and test function. The test function can optionally be configured with test options.
test('Multiplier', () => {
let let double: any
double = import multiplier
multiplier(0, 2);
expect<any>(actual: any, message?: string): Assertion<any> (+1 overload)
expect(let double: any
double.value).JestAssertion<any>.toEqual: <number>(expected: number) => void
toEqual(0);
let double: any
double.set(5);
expect<any>(actual: any, message?: string): Assertion<any> (+1 overload)
expect(let double: any
double.value).JestAssertion<any>.toEqual: <number>(expected: number) => void
toEqual(10);
});
在测试文件中使用符文(runes)
您可以在测试文件中使用符文。首先确保您的打包工具知道在运行测试之前要通过 Svelte 编译器处理文件,方法是在文件名中添加 .svelte
(例如 multiplier.svelte.test.js
)。之后,您就可以在测试中使用符文了。
import { function flushSync(fn?: (() => void) | undefined): void
Synchronously flushes any pending state changes and those that result from it.
flushSync } from 'svelte';
import { const expect: ExpectStatic
expect, const test: TestAPI
Defines a test case with a given name and test function. The test function can optionally be configured with test options.
test } from 'vitest';
import { import multiplier
multiplier } from './multiplier.svelte.js';
test<object>(name: string | Function, fn?: TestFunction<object> | undefined, options?: number | TestOptions): void (+2 overloads)
Defines a test case with a given name and test function. The test function can optionally be configured with test options.
test('Multiplier', () => {
let let count: number
count = function $state<0>(initial: 0): 0 (+1 overload)
namespace $state
$state(0);
let let double: any
double = import multiplier
multiplier(() => let count: number
count, 2);
expect<any>(actual: any, message?: string): Assertion<any> (+1 overload)
expect(let double: any
double.value).JestAssertion<any>.toEqual: <number>(expected: number) => void
toEqual(0);
let count: number
count = 5;
expect<any>(actual: any, message?: string): Assertion<any> (+1 overload)
expect(let double: any
double.value).JestAssertion<any>.toEqual: <number>(expected: number) => void
toEqual(10);
});
如果被测试的代码使用了 effects,您需要将测试包装在 $effect.root
中:
import { function flushSync(fn?: (() => void) | undefined): void
Synchronously flushes any pending state changes and those that result from it.
flushSync } from 'svelte';
import { const expect: ExpectStatic
expect, const test: TestAPI
Defines a test case with a given name and test function. The test function can optionally be configured with test options.
test } from 'vitest';
import { import logger
logger } from './logger.svelte.js';
test<object>(name: string | Function, fn?: TestFunction<object> | undefined, options?: number | TestOptions): void (+2 overloads)
Defines a test case with a given name and test function. The test function can optionally be configured with test options.
test('Effect', () => {
const const cleanup: () => void
cleanup = namespace $effect
function $effect(fn: () => void | (() => void)): void
Runs code when a component is mounted to the DOM, and then whenever its dependencies change, i.e. $state
or $derived
values.
The timing of the execution is after the DOM has been updated.
Example:
$effect(() => console.log('The count is now ' + count));
If you return a function from the effect, it will be called right before the effect is run again, or when the component is unmounted.
Does not run during server side rendering.
$effect.function $effect.root(fn: () => void | (() => void)): () => void
The $effect.root
rune is an advanced feature that creates a non-tracked scope that doesn’t auto-cleanup. This is useful for
nested effects that you want to manually control. This rune also allows for creation of effects outside of the component
initialisation phase.
Example:
<script>
let count = $state(0);
const cleanup = $effect.root(() => {
$effect(() => {
console.log(count);
})
return () => {
console.log('effect root cleanup');
}
});
</script>
<button onclick={() => cleanup()}>cleanup</button>
root(() => {
let let count: number
count = function $state<0>(initial: 0): 0 (+1 overload)
namespace $state
$state(0);
// logger 使用 $effect 来记录其输入的更新
let let log: any
log = import logger
logger(() => let count: number
count);
// 效果通常在微任务后运行,
// 使用 flushSync 同步执行所有待处理的效果
function flushSync(fn?: (() => void) | undefined): void
Synchronously flushes any pending state changes and those that result from it.
flushSync();
expect<any>(actual: any, message?: string): Assertion<any> (+1 overload)
expect(let log: any
log.value).JestAssertion<any>.toEqual: <number[]>(expected: number[]) => void
toEqual([0]);
let count: number
count = 1;
function flushSync(fn?: (() => void) | undefined): void
Synchronously flushes any pending state changes and those that result from it.
flushSync();
expect<any>(actual: any, message?: string): Assertion<any> (+1 overload)
expect(let log: any
log.value).JestAssertion<any>.toEqual: <number[]>(expected: number[]) => void
toEqual([0, 1]);
});
const cleanup: () => void
cleanup();
});
组件测试
您可以使用 Vitest 在隔离环境下测试您的组件。
在编写组件测试之前,请考虑是否真的需要测试组件本身,还是更多地关注组件内部的逻辑。如果是后者,考虑将逻辑提取出来单独测试,避免组件测试的开销
首先安装 jsdom(一个提供 DOM API 模拟的库):
npm install -D jsdom
然后调整您的 vite.config.js
:
import { function defineConfig(config: UserConfig): UserConfig (+3 overloads)
defineConfig } from 'vitest/config';
export default function defineConfig(config: UserConfig): UserConfig (+3 overloads)
defineConfig({
UserConfig.plugins?: PluginOption[] | undefined
Array of vite plugins to use.
plugins: [
/* ... */
],
UserConfig.test?: InlineConfig | undefined
Options for Vitest
test: {
// 如果您在客户端测试组件,您需要设置 DOM 环境。
// 如果不是所有文件都需要这个环境,您可以在测试文件顶部使用
// `// @vitest-environment jsdom` 注释来代替。
InlineConfig.environment?: VitestEnvironment | undefined
Running environment
Supports ‘node’, ‘jsdom’, ‘happy-dom’, ‘edge-runtime’
If used unsupported string, will try to load the package vitest-environment-${env}
environment: 'jsdom'
},
// 告诉 Vitest 使用 `package.json` 文件中的 `browser` 入口点,即使它在 Node 中运行
UserConfig.resolve?: (ResolveOptions & {
alias?: AliasOptions;
}) | undefined
Configure resolver
resolve: var process: NodeJS.Process
process.NodeJS.Process.env: NodeJS.ProcessEnv
The process.env
property returns an object containing the user environment.
See environ(7)
.
An example of this object looks like:
{
TERM: 'xterm-256color',
SHELL: '/usr/local/bin/bash',
USER: 'maciej',
PATH: '~/.bin/:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin',
PWD: '/Users/maciej',
EDITOR: 'vim',
SHLVL: '1',
HOME: '/Users/maciej',
LOGNAME: 'maciej',
_: '/usr/local/bin/node'
}
It is possible to modify this object, but such modifications will not be
reflected outside the Node.js process, or (unless explicitly requested)
to other Worker
threads.
In other words, the following example would not work:
node -e 'process.env.foo = "bar"' &#x26;&#x26; echo $foo
While the following will:
import { env } from 'node:process';
env.foo = 'bar';
console.log(env.foo);
Assigning a property on process.env
will implicitly convert the value
to a string. This behavior is deprecated. Future versions of Node.js may
throw an error when the value is not a string, number, or boolean.
import { env } from 'node:process';
env.test = null;
console.log(env.test);
// => 'null'
env.test = undefined;
console.log(env.test);
// => 'undefined'
Use delete
to delete a property from process.env
.
import { env } from 'node:process';
env.TEST = 1;
delete env.TEST;
console.log(env.TEST);
// => undefined
On Windows operating systems, environment variables are case-insensitive.
import { env } from 'node:process';
env.TEST = 1;
console.log(env.test);
// => 1
Unless explicitly specified when creating a Worker
instance,
each Worker
thread has its own copy of process.env
, based on its
parent thread’s process.env
, or whatever was specified as the env
option
to the Worker
constructor. Changes to process.env
will not be visible
across Worker
threads, and only the main thread can make changes that
are visible to the operating system or to native add-ons. On Windows, a copy of process.env
on a Worker
instance operates in a case-sensitive manner
unlike the main thread.
env.string | undefined
VITEST
? {
ResolveOptions.conditions?: string[] | undefined
conditions: ['browser']
}
: var undefined
undefined
});
之后,您可以创建一个测试文件,在其中导入要测试的组件,以编程方式与之交互并编写关于结果的预期:
import { function flushSync(fn?: (() => void) | undefined): void
Synchronously flushes any pending state changes and those that result from it.
flushSync, function mount<Props extends Record<string, any>, Exports extends Record<string, any>>(component: ComponentType<SvelteComponent<Props>> | Component<Props, Exports, any>, options: MountOptions<Props>): Exports
Mounts a component to the given target and returns the exports and potentially the props (if compiled with accessors: true
) of the component.
Transitions will play during the initial render unless the intro
option is set to false
.
mount, function unmount(component: Record<string, any>, options?: {
outro?: boolean;
} | undefined): Promise<void>
Unmounts a component that was previously mounted using mount
or hydrate
.
Since 5.13.0, if options.outro
is true
, transitions will play before the component is removed from the DOM.
Returns a Promise
that resolves after transitions have completed if options.outro
is true, or immediately otherwise (prior to 5.13.0, returns void
).
import { mount, unmount } from 'svelte';
import App from './App.svelte';
const app = mount(App, { target: document.body });
// later...
unmount(app, { outro: true });
unmount } from 'svelte';
import { const expect: ExpectStatic
expect, const test: TestAPI
Defines a test case with a given name and test function. The test function can optionally be configured with test options.
test } from 'vitest';
import type Component = SvelteComponent<Record<string, any>, any, any>
const Component: LegacyComponentType
Component from './Component.svelte';
test<object>(name: string | Function, fn?: TestFunction<object> | undefined, options?: number | TestOptions): void (+2 overloads)
Defines a test case with a given name and test function. The test function can optionally be configured with test options.
test('Component', () => {
// 使用 Svelte 的 `mount` API 实例化组件
const const component: {
$on?(type: string, callback: (e: any) => void): () => void;
$set?(props: Partial<Record<string, any>>): void;
} & Record<string, any>
component = mount<Record<string, any>, {
$on?(type: string, callback: (e: any) => void): () => void;
$set?(props: Partial<Record<string, any>>): void;
} & Record<...>>(component: ComponentType<...> | Component<...>, options: MountOptions<...>): {
...;
} & Record<...>
Mounts a component to the given target and returns the exports and potentially the props (if compiled with accessors: true
) of the component.
Transitions will play during the initial render unless the intro
option is set to false
.
mount(const Component: LegacyComponentType
Component, {
target: Document | Element | ShadowRoot
Target element where the component will be mounted.
target: var document: Document
document.Document.body: HTMLElement
Specifies the beginning and end of the document body.
body, // 由于 jsdom 的存在,`document` 是可用的
props?: Record<string, any> | undefined
Component properties.
props: { initial: number
initial: 0 }
});
expect<string>(actual: string, message?: string): Assertion<string> (+1 overload)
expect(var document: Document
document.Document.body: HTMLElement
Specifies the beginning and end of the document body.
body.InnerHTML.innerHTML: string
innerHTML).JestAssertion<string>.toBe: <string>(expected: string) => void
toBe('<button>0</button>');
// 点击按钮,然后刷新更改,这样您就可以同步地编写期望
var document: Document
document.Document.body: HTMLElement
Specifies the beginning and end of the document body.
body.ParentNode.querySelector<"button">(selectors: "button"): HTMLButtonElement | null (+4 overloads)
Returns the first element that is a descendant of node that matches selectors.
querySelector('button').HTMLElement.click(): void
click();
function flushSync(fn?: (() => void) | undefined): void
Synchronously flushes any pending state changes and those that result from it.
flushSync();
expect<string>(actual: string, message?: string): Assertion<string> (+1 overload)
expect(var document: Document
document.Document.body: HTMLElement
Specifies the beginning and end of the document body.
body.InnerHTML.innerHTML: string
innerHTML).JestAssertion<string>.toBe: <string>(expected: string) => void
toBe('<button>1</button>');
// 从 DOM 中移除组件
function unmount(component: Record<string, any>, options?: {
outro?: boolean;
} | undefined): Promise<void>
Unmounts a component that was previously mounted using mount
or hydrate
.
Since 5.13.0, if options.outro
is true
, transitions will play before the component is removed from the DOM.
Returns a Promise
that resolves after transitions have completed if options.outro
is true, or immediately otherwise (prior to 5.13.0, returns void
).
import { mount, unmount } from 'svelte';
import App from './App.svelte';
const app = mount(App, { target: document.body });
// later...
unmount(app, { outro: true });
unmount(const component: {
$on?(type: string, callback: (e: any) => void): () => void;
$set?(props: Partial<Record<string, any>>): void;
} & Record<string, any>
component);
});
虽然这个过程非常简单明了,但它也比较底层且有些脆弱,因为组件的精确结构可能会频繁变化。像 @testing-library/svelte 这样的工具可以帮助简化测试。上面的测试可以重写为:
import { function render<C extends unknown, Q extends Queries = typeof import("/vercel/path0/node_modules/.pnpm/@testing-library+dom@10.4.0/node_modules/@testing-library/dom/types/queries")>(Component: ComponentType<...>, options?: SvelteComponentOptions<C>, renderOptions?: RenderOptions<Q>): RenderResult<C, Q>
Render a component into the document.
render, const screen: Screen<typeof import("/vercel/path0/node_modules/.pnpm/@testing-library+dom@10.4.0/node_modules/@testing-library/dom/types/queries")>
screen } from '@testing-library/svelte';
import const userEvent: {
readonly setup: typeof setupMain;
readonly clear: typeof clear;
readonly click: typeof click;
readonly copy: typeof copy;
... 12 more ...;
readonly tab: typeof tab;
}
userEvent from '@testing-library/user-event';
import { const expect: ExpectStatic
expect, const test: TestAPI
Defines a test case with a given name and test function. The test function can optionally be configured with test options.
test } from 'vitest';
import type Component = SvelteComponent<Record<string, any>, any, any>
const Component: LegacyComponentType
Component from './Component.svelte';
test<object>(name: string | Function, fn?: TestFunction<object> | undefined, options?: number | TestOptions): void (+2 overloads)
Defines a test case with a given name and test function. The test function can optionally be configured with test options.
test('Component', async () => {
const const user: UserEvent
user = const userEvent: {
readonly setup: typeof setupMain;
readonly clear: typeof clear;
readonly click: typeof click;
readonly copy: typeof copy;
... 12 more ...;
readonly tab: typeof tab;
}
userEvent.setup: (options?: Options) => UserEvent
Start a “session” with userEvent.
All APIs returned by this function share an input device state and a default configuration.
setup();
render<SvelteComponent<Record<string, any>, any, any>, typeof import("/vercel/path0/node_modules/.pnpm/@testing-library+dom@10.4.0/node_modules/@testing-library/dom/types/queries")>(Component: ComponentType<...>, options?: SvelteComponentOptions<...> | undefined, renderOptions?: RenderOptions<...> | undefined): RenderResult<...>
Render a component into the document.
render(const Component: LegacyComponentType
Component);
const const button: HTMLElement
button = const screen: Screen<typeof import("/vercel/path0/node_modules/.pnpm/@testing-library+dom@10.4.0/node_modules/@testing-library/dom/types/queries")>
screen.getByRole<HTMLElement>(role: ByRoleMatcher, options?: ByRoleOptions | undefined): HTMLElement (+1 overload)
getByRole('button');
expect<HTMLElement>(actual: HTMLElement, message?: string): Assertion<HTMLElement> (+1 overload)
expect(const button: HTMLElement
button).toHaveTextContent(0);
await const user: UserEvent
user.click: (element: Element) => Promise<void>
click(const button: HTMLElement
button);
expect<HTMLElement>(actual: HTMLElement, message?: string): Assertion<HTMLElement> (+1 overload)
expect(const button: HTMLElement
button).toHaveTextContent(1);
});
当编写涉及双向绑定、上下文或代码片段属性的组件测试时,最好为您的特定测试创建一个包装组件并与之交互。@testing-library/svelte
包含一些示例。
使用 Playwright 进行端到端测试
E2E(端到端的简称)测试允许您从用户的视角测试整个应用程序。本节以 Playwright 为例,但您也可以使用其他解决方案,如 Cypress 或 NightwatchJS。
要开始使用 Playwright,可以通过 VS Code 扩展 安装,或者使用 npm init playwright
从命令行安装。它也是运行 npx sv create
时设置 CLI 的一部分。
完成后,您应该有一个 tests
文件夹和一个 Playwright 配置。您可能需要调整该配置,告诉 Playwright 在运行测试之前要做什么 - 主要是在某个端口启动您的应用程序:
const const config: {
webServer: {
command: string;
port: number;
};
testDir: string;
testMatch: RegExp;
}
config = {
webServer: {
command: string;
port: number;
}
webServer: {
command: string
command: 'npm run build && npm run preview',
port: number
port: 4173
},
testDir: string
testDir: 'tests',
testMatch: RegExp
testMatch: /(.+\.)?(test|spec)\.[jt]s/
};
export default const config: {
webServer: {
command: string;
port: number;
};
testDir: string;
testMatch: RegExp;
}
config;
现在您可以开始编写测试了。这些测试完全不知道 Svelte 作为一个框架的存在,所以您主要是和 DOM 交互并编写断言。
import { import expect
expect, import test
test } from '@playwright/test';
import test
test('主页包含预期的 h1', async ({ page }) => {
await page: any
page.goto('/');
await import expect
expect(page: any
page.locator('h1')).toBeVisible();
});