測試
測試可協助您撰寫和維護程式碼,並防止迴歸。測試框架可協助您做到這一點,讓您可以描述程式碼應如何運作的斷言或期望。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({
// ...
// Tell Vitest to use the `browser` entry points in `package.json` files, even though it's running in 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);
});
在您的測試檔案中使用符文
可以在您的測試檔案中使用符文。首先,請確保您的綁定器知道在執行測試之前將檔案路由到 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
Declares reactive state.
Example:
let count = $state(0);
$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);
});
如果正在測試的程式碼使用效果,您需要將測試包裝在 $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
Declares reactive state.
Example:
let count = $state(0);
$state(0);
// logger uses an $effect to log updates of its input
let let log: any
log = import logger
logger(() => let count: number
count);
// effects normally run after a microtask,
// use flushSync to execute all pending effects synchronously
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: {
// 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 | 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'
},
// Tell Vitest to use the `browser` entry points in `package.json` files, even though it's running in 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>): void
Unmounts a component that was previously mounted using mount
or hydrate
.
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', () => {
// 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<...>): {
...;
} & 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, // `document` exists because of jsdom
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>');
// Click the button, then flush the changes so you can synchronously write expectations
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>');
// Remove the component from the DOM
function unmount(component: Record<string, any>): void
Unmounts a component that was previously mounted using mount
or hydrate
.
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);
});
在撰寫涉及雙向繫結、上下文或程式碼片段 prop 的組件測試時,最好為您的特定測試建立一個包裝函式組件,並與之互動。@testing-library/svelte
包含一些範例。
使用 Playwright 進行 E2E 測試
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('home page has expected h1', async ({ page }) => {
await page: any
page.goto('/');
await import expect
expect(page: any
page.locator('h1')).toBeVisible();
});