测试
测试可帮助你编写和维护代码并防止回归。测试框架可以帮助你实现这一点,允许你描述有关代码应如何表现的断言或期望。Svelte 不会干涉你使用哪种测试框架 — 你可以使用 Vitest、Jasmine、Cypress 和 Playwright 等解决方案编写单元测试、集成测试和端到端测试。
¥Testing helps you write and maintain your code and guard against regressions. Testing frameworks help you with that, allowing you to describe assertions or expectations about how your code should behave. Svelte is unopinionated about which testing framework you use — you can write unit tests, integration tests, and end-to-end tests using solutions like Vitest, Jasmine, Cypress and Playwright.
使用 Vitest 进行单元和组件测试(Unit and component tests with Vitest)
¥Unit and component tests with Vitest
单元测试允许你测试代码的小部分隔离部分。集成测试允许你测试应用的各个部分,以查看它们是否可以协同工作。如果你使用的是 Vite(包括通过 SvelteKit),我们建议使用 Vitest。你可以在项目创建期间或之后使用 Svelte CLI 来 设置 Vitest。
¥Unit tests allow you to test small isolated parts of your code. Integration tests allow you to test parts of your application to see if they work together. If you’re using Vite (including via SvelteKit), we recommend using Vitest. You can use the Svelte CLI to setup Vitest either during project creation or later on.
要手动设置 Vitest,请先安装它:
¥To setup Vitest manually, first install it:
npm install -D vitest然后调整你的 vite.config.js:
¥Then adjust your 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({
// ...
// Tell Vitest to use the `browser` entry points in `package.json` files, even though it's running in Node
resolve?: AllResolveOptions | undefinedresolve: var process: NodeJS.Processprocess.NodeJS.Process.env: NodeJS.ProcessEnvThe 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 | undefinedVITEST
? {
EnvironmentResolveOptions.conditions?: string[] | undefinedconditions: ['browser']
}
: var undefinedundefined
});如果由于(例如)你还要测试后端库而不希望加载所有软件包的浏览器版本,则 you may need to resort to an alias configuration
你现在可以在 .js/.ts 文件中为代码编写单元测试:
¥You can now write unit tests for code inside your .js/.ts files:
import { function flushSync<T = void>(fn?: (() => T) | undefined): TSynchronously flush any pending updates.
Returns void if no callback is provided, otherwise returns the result of calling the callback.
flushSync } from 'svelte';
import { const expect: ExpectStaticexpect, const test: TestAPIDefines 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 multipliermultiplier } from './multiplier.svelte.js';
test<object>(name: string | Function, fn?: TestFunction<object> | undefined, options?: number | TestCollectorOptions): 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: anydouble = import multipliermultiplier(0, 2);
expect<any>(actual: any, message?: string): Assertion<any> (+1 overload)expect(let double: anydouble.value).JestAssertion<any>.toEqual: <number>(expected: number) => voidUsed when you want to check that two objects have the same value.
This matcher recursively checks the equality of all fields, rather than checking for object identity.
toEqual(0);
let double: anydouble.set(5);
expect<any>(actual: any, message?: string): Assertion<any> (+1 overload)expect(let double: anydouble.value).JestAssertion<any>.toEqual: <number>(expected: number) => voidUsed when you want to check that two objects have the same value.
This matcher recursively checks the equality of all fields, rather than checking for object identity.
toEqual(10);
});/**
* @param {number} initial
* @param {number} k
*/
export function function multiplier(initial: number, k: number): {
readonly value: number;
set: (c: number) => void;
}
multiplier(initial: numberinitial, k: numberk) {
let let count: numbercount = function $state<number>(initial: number): number (+1 overload)
namespace $state
$state(initial: numberinitial);
return {
get value: numbervalue() {
return let count: numbercount * k: numberk;
},
/** @param {number} c */
set: (c: number) => voidset: (c: numberc) => {
let count: numbercount = c: numberc;
}
};
}export function function multiplier(initial: number, k: number): {
readonly value: number;
set: (c: number) => void;
}
multiplier(initial: numberinitial: number, k: numberk: number) {
let let count: numbercount = function $state<number>(initial: number): number (+1 overload)
namespace $state
$state(initial: numberinitial);
return {
get value: numbervalue() {
return let count: numbercount * k: numberk;
},
set: (c: number) => voidset: (c: numberc: number) => {
let count: numbercount = c: numberc;
}
};
}在测试文件中使用符文(Using runes inside your test files)
¥Using runes inside your test files
由于 Vitest 以与源文件相同的方式处理你的测试文件,因此只要文件名包含 .svelte,你就可以在测试中使用符文:
¥Since Vitest processes your test files the same way as your source files, you can use runes inside your tests as long as the filename includes .svelte:
import { function flushSync<T = void>(fn?: (() => T) | undefined): TSynchronously flush any pending updates.
Returns void if no callback is provided, otherwise returns the result of calling the callback.
flushSync } from 'svelte';
import { const expect: ExpectStaticexpect, const test: TestAPIDefines 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 multipliermultiplier } from './multiplier.svelte.js';
test<object>(name: string | Function, fn?: TestFunction<object> | undefined, options?: number | TestCollectorOptions): 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: numbercount = function $state<0>(initial: 0): 0 (+1 overload)
namespace $state
$state(0);
let let double: anydouble = import multipliermultiplier(() => let count: numbercount, 2);
expect<any>(actual: any, message?: string): Assertion<any> (+1 overload)expect(let double: anydouble.value).JestAssertion<any>.toEqual: <number>(expected: number) => voidUsed when you want to check that two objects have the same value.
This matcher recursively checks the equality of all fields, rather than checking for object identity.
toEqual(0);
let count: numbercount = 5;
expect<any>(actual: any, message?: string): Assertion<any> (+1 overload)expect(let double: anydouble.value).JestAssertion<any>.toEqual: <number>(expected: number) => voidUsed when you want to check that two objects have the same value.
This matcher recursively checks the equality of all fields, rather than checking for object identity.
toEqual(10);
});/**
* @param {() => number} getCount
* @param {number} k
*/
export function function multiplier(getCount: () => number, k: number): {
readonly value: number;
}
multiplier(getCount: () => numbergetCount, k: numberk) {
return {
get value: numbervalue() {
return getCount: () => numbergetCount() * k: numberk;
}
};
}export function function multiplier(getCount: () => number, k: number): {
readonly value: number;
}
multiplier(getCount: () => numbergetCount: () => number, k: numberk: number) {
return {
get value: numbervalue() {
return getCount: () => numbergetCount() * k: numberk;
}
};
}如果正在测试的代码使用效果,则需要将测试封装在 $effect.root 内:
¥If the code being tested uses effects, you need to wrap the test inside $effect.root:
import { function flushSync<T = void>(fn?: (() => T) | undefined): TSynchronously flush any pending updates.
Returns void if no callback is provided, otherwise returns the result of calling the callback.
flushSync } from 'svelte';
import { const expect: ExpectStaticexpect, const test: TestAPIDefines 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 loggerlogger } from './logger.svelte.js';
test<object>(name: string | Function, fn?: TestFunction<object> | undefined, options?: number | TestCollectorOptions): 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: () => voidcleanup = 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)): () => voidThe $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: numbercount = function $state<0>(initial: 0): 0 (+1 overload)
namespace $state
$state(0);
// logger uses an $effect to log updates of its input
let let log: anylog = import loggerlogger(() => let count: numbercount);
// effects normally run after a microtask,
// use flushSync to execute all pending effects synchronously
flushSync<void>(fn?: (() => void) | undefined): voidSynchronously flush any pending updates.
Returns void if no callback is provided, otherwise returns the result of calling the callback.
flushSync();
expect<any>(actual: any, message?: string): Assertion<any> (+1 overload)expect(let log: anylog).JestAssertion<any>.toEqual: <number[]>(expected: number[]) => voidUsed when you want to check that two objects have the same value.
This matcher recursively checks the equality of all fields, rather than checking for object identity.
toEqual([0]);
let count: numbercount = 1;
flushSync<void>(fn?: (() => void) | undefined): voidSynchronously flush any pending updates.
Returns void if no callback is provided, otherwise returns the result of calling the callback.
flushSync();
expect<any>(actual: any, message?: string): Assertion<any> (+1 overload)expect(let log: anylog).JestAssertion<any>.toEqual: <number[]>(expected: number[]) => voidUsed when you want to check that two objects have the same value.
This matcher recursively checks the equality of all fields, rather than checking for object identity.
toEqual([0, 1]);
});
const cleanup: () => voidcleanup();
});/**
* @param {() => any} getValue
*/
export function function logger(getValue: () => any): any[]logger(getValue: () => anygetValue) {
/** @type {any[]} */
let let log: any[]log = [];
function $effect(fn: () => void | (() => void)): void
namespace $effect
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(() => {
let log: any[]log.Array<any>.push(...items: any[]): numberAppends new elements to the end of an array, and returns the new length of the array.
push(getValue: () => anygetValue());
});
return let log: any[]log;
}export function function logger(getValue: () => any): any[]logger(getValue: () => anygetValue: () => any) {
let let log: any[]log: any[] = [];
function $effect(fn: () => void | (() => void)): void
namespace $effect
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(() => {
let log: any[]log.Array<any>.push(...items: any[]): numberAppends new elements to the end of an array, and returns the new length of the array.
push(getValue: () => anygetValue());
});
return let log: any[]log;
}组件测试(Component testing)
¥Component testing
可以单独测试你的组件,这允许你在浏览器(真实或模拟)中渲染它们、模拟行为并进行断言,而无需启动整个应用。
¥It is possible to test your components in isolation, which allows you to render them in a browser (real or simulated), simulate behavior, and make assertions, without spinning up your whole app.
在编写组件测试之前,请考虑你是否真的需要测试组件,或者是否更关注组件内部的逻辑。如果是,请考虑提取该逻辑进行单独测试,而无需组件开销。
要开始,请安装 jsdom(一个填充 DOM API 的库):
¥To get started, install jsdom (a library that shims DOM APIs):
npm install -D jsdom然后调整你的 vite.config.js:
¥Then adjust your 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[] | undefinedArray of vite plugins to use.
plugins: [
/* ... */
],
UserConfig.test?: InlineConfig | undefinedOptions for Vitest
test: {
// If you are testing components client-side, you need to setup 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.
InlineConfig.environment?: VitestEnvironment | undefinedRunning environment
Supports ‘node’, ‘jsdom’, ‘happy-dom’, ‘edge-runtime’
If used unsupported string, will try to load the package vitest-environment-${env}
environment: 'jsdom'
},
// Tell Vitest to use the `browser` entry points in `package.json` files, even though it's running in Node
resolve?: AllResolveOptions | undefinedresolve: var process: NodeJS.Processprocess.NodeJS.Process.env: NodeJS.ProcessEnvThe 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 | undefinedVITEST
? {
EnvironmentResolveOptions.conditions?: string[] | undefinedconditions: ['browser']
}
: var undefinedundefined
});之后,你可以创建一个测试文件,在其中导入要测试的组件,以编程方式与其交互并写下对结果的期望:
¥After that, you can create a test file in which you import the component to test, interact with it programmatically and write expectations about the results:
import { function flushSync<T = void>(fn?: (() => T) | undefined): TSynchronously flush any pending updates.
Returns void if no callback is provided, otherwise returns the result of calling the callback.
flushSync, function mount<Props extends Record<string, any>, Exports extends Record<string, any>>(component: ComponentType<SvelteComponent<Props>> | Component<Props, Exports, any>, options: MountOptions<Props>): ExportsMounts 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: ExpectStaticexpect, const test: TestAPIDefines 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 | TestCollectorOptions): 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', () => {
// Instantiate the component using Svelte's `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<...>): {
$on?(type: string, callback: (e: any) => void): () => void;
$set?(props: Partial<Record<string, any>>): void;
} & 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: LegacyComponentTypeComponent, {
target: Document | Element | ShadowRootTarget element where the component will be mounted.
target: var document: Documentdocument.Document.body: HTMLElementSpecifies the beginning and end of the document body.
body, // `document` exists because of jsdom
props?: Record<string, any> | undefinedComponent properties.
props: { initial: numberinitial: 0 }
});
expect<string>(actual: string, message?: string): Assertion<string> (+1 overload)expect(var document: Documentdocument.Document.body: HTMLElementSpecifies the beginning and end of the document body.
body.Element.innerHTML: stringinnerHTML).JestAssertion<string>.toBe: <string>(expected: string) => voidChecks that a value is what you expect. It calls Object.is to compare values.
Don’t use toBe with floating-point numbers.
toBe('<button>0</button>');
// Click the button, then flush the changes so you can synchronously write expectations
var document: Documentdocument.Document.body: HTMLElementSpecifies 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(): voidclick();
flushSync<void>(fn?: (() => void) | undefined): voidSynchronously flush any pending updates.
Returns void if no callback is provided, otherwise returns the result of calling the callback.
flushSync();
expect<string>(actual: string, message?: string): Assertion<string> (+1 overload)expect(var document: Documentdocument.Document.body: HTMLElementSpecifies the beginning and end of the document body.
body.Element.innerHTML: stringinnerHTML).JestAssertion<string>.toBe: <string>(expected: string) => voidChecks that a value is what you expect. It calls Object.is to compare values.
Don’t use toBe with floating-point numbers.
toBe('<button>1</button>');
// Remove the component from the 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 这样的工具可以帮助简化你的测试。上面的测试可以像这样重写:
¥While the process is very straightforward, it is also low level and somewhat brittle, as the precise structure of your component may change frequently. Tools like @testing-library/svelte can help streamline your tests. The above test could be rewritten like this:
import { function render<C extends unknown, Q extends Queries = typeof import("/Users/q/code/nodejscn/svelte/src/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("/Users/q/code/nodejscn/svelte/src/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: ExpectStaticexpect, const test: TestAPIDefines 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 | TestCollectorOptions): 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: UserEventuser = 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) => UserEventStart 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("/Users/q/code/nodejscn/svelte/src/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: LegacyComponentTypeComponent);
const const button: HTMLElementbutton = const screen: Screen<typeof import("/Users/q/code/nodejscn/svelte/src/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: HTMLElementbutton).toHaveTextContent(0);
await const user: UserEventuser.click: (element: Element) => Promise<void>click(const button: HTMLElementbutton);
expect<HTMLElement>(actual: HTMLElement, message?: string): Assertion<HTMLElement> (+1 overload)expect(const button: HTMLElementbutton).toHaveTextContent(1);
});在编写涉及双向绑定、上下文或代码片段属性的组件测试时,最好为特定测试创建一个封装器组件并与其交互。@testing-library/svelte 包含一些 示例。
¥When writing component tests that involve two-way bindings, context or snippet props, it’s best to create a wrapper component for your specific test and interact with that. @testing-library/svelte contains some examples.
使用 Storybook 进行组件测试(Component tests with Storybook)
¥Component tests with Storybook
Storybook 是一个用于开发和记录 UI 组件的工具,也可用于测试你的组件。它们在 Vitest 的浏览器模式下运行,该模式会在真实的浏览器中渲染你的组件,以获得最真实的测试环境。
¥Storybook is a tool for developing and documenting UI components, and it can also be used to test your components. They’re run with Vitest’s browser mode, which renders your components in a real browser for the most realistic testing environment.
首先,通过 npx sv add storybook 在你的项目中安装 Storybook (使用 Svelte 的 CLI),并选择包含测试功能的推荐配置。如果你已经在使用 Storybook,并且想了解更多关于 Storybook 测试功能的信息,请按照 Storybook 测试文档 开始操作。
¥To get started, first install Storybook (using Svelte’s CLI) in your project via npx sv add storybook and choose the recommended configuration that includes testing features. If you’re already using Storybook, and for more information on Storybook’s testing capabilities, follow the Storybook testing docs to get started.
你可以为组件变体创建故事,并使用 播放功能 测试交互,这允许你使用测试库和 Vitest API 模拟行为并进行断言。以下是两个可供测试的故事示例,一个故事渲染一个空的 LoginForm 组件,另一个故事模拟用户填写表单:
¥You can create stories for component variations and test interactions with the play function, which allows you to simulate behavior and make assertions using the Testing Library and Vitest APIs. Here’s an example of two stories that can be tested, one that renders an empty LoginForm component and one that simulates a user filling out the form:
<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();
}}
/>使用 Playwright 进行端到端测试(End-to-end tests with Playwright)
¥End-to-end tests with Playwright
E2E(’eslint (linting)’ 的缩写)测试允许你通过用户的眼睛测试完整的应用。本节以 Playwright 为例,但你也可以使用其他解决方案,如 Cypress 或 NightwatchJS。
¥E2E (short for ‘end to end’) tests allow you to test your full application through the eyes of the user. This section uses Playwright as an example, but you can also use other solutions like Cypress or NightwatchJS.
你可以在项目创建期间或之后使用 Svelte CLI 来 设置 Playwright。你也可以使用 使用 npm init playwright 设置。此外,你可能还需要安装 IDE 插件(例如 VS Code 扩展),以便能够从 IDE 内部执行测试。
¥You can use the Svelte CLI to setup Playwright either during project creation or later on. You can also set it up with npm init playwright. Additionally, you may also want to install an IDE plugin such as the VS Code extension to be able to execute tests from inside your IDE.
如果你已经运行 npm init playwright 或未使用 Vite,则可能需要调整 Playwright 配置,以在运行测试之前告知 Playwright 应执行的操作。 - 主要用于在特定端口启动你的应用。例如:
¥If you’ve run npm init playwright or are not using Vite, you may need to adjust the Playwright config to tell Playwright what to do before running the tests - mainly starting your application at a certain port. For example:
const const config: {
webServer: {
command: string;
port: number;
};
testDir: string;
testMatch: RegExp;
}
config = {
webServer: {
command: string;
port: number;
}
webServer: {
command: stringcommand: 'npm run build && npm run preview',
port: numberport: 4173
},
testDir: stringtestDir: 'tests',
testMatch: RegExptestMatch: /(.+\.)?(test|spec)\.[jt]s/
};
export default const config: {
webServer: {
command: string;
port: number;
};
testDir: string;
testMatch: RegExp;
}
config;你现在可以开始编写测试了。这些完全不知道 Svelte 是一个框架,因此你主要与 DOM 交互并编写断言。
¥You can now start writing tests. These are totally unaware of Svelte as a framework, so you mainly interact with the DOM and write assertions.
import { import expectexpect, import testtest } from '@playwright/test';
import testtest('home page has expected h1', async ({ page }) => {
await page: anypage.goto('/');
await import expectexpect(page: anypage.locator('h1')).toBeVisible();
});