テスト
テストは、コードの記述と保守、そしてリグレッションを防ぐのに役立ちます。テストフレームワークは、コードの動作に関するアサーションや期待を記述することを可能にし、テストを支援します。 Svelte は、使用するテストフレームワークについて意見を強制しません。Vitest、Jasmine、Cypress、Playwright などのソリューションを使用して、ユニットテスト、統合テスト、エンドツーエンドテストを作成できます。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.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
? {
ResolveOptions.conditions?: string[] | undefinedconditions: ['browser']
}
: var undefinedundefined
});バックエンドライブラリもテストする場合など、すべてのパッケージのブラウザバージョンを読み込むことが望ましくない場合は、エイリアス設定に頼る必要がある場合があります
これで、`.js/.ts` ファイル内のコードのユニットテストを作成できます
import { function flushSync(fn?: (() => void) | undefined): voidSynchronously flushes any pending state changes and those that result from it.
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.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: 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) => voidtoEqual(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) => voidtoEqual(10);
});テストファイル内でルーンを使用する
テストファイル内でルーンを使用することができます。最初に、ファイル名に `.svelte` を追加する(例:`multiplier.svelte.test.js`)ことで、テストを実行する前にバンドラーがファイルを Svelte コンパイラにルーティングすることを認識していることを確認します。その後、テスト内でルーンを使用できます。
import { function flushSync(fn?: (() => void) | undefined): voidSynchronously flushes any pending state changes and those that result from it.
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 | 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: numbercount = function $state<0>(initial: 0): 0 (+1 overload)
namespace $state
Declares reactive state.
Example:
let count = $state(0);
$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) => voidtoEqual(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) => voidtoEqual(10);
});テスト対象のコードがエフェクトを使用している場合は、`$effect.root` 内にテストをラップする必要があります
import { function flushSync(fn?: (() => void) | undefined): voidSynchronously flushes any pending state changes and those that result from it.
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 | 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: () => 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>
https://svelte.dokyumento.jp/docs/svelte/$effect#$effect.root
root(() => {
let let count: numbercount = 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: anylog = import loggerlogger(() => let count: numbercount);
// effects normally run after a microtask,
// use flushSync to execute all pending effects synchronously
function flushSync(fn?: (() => void) | undefined): voidSynchronously 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: anylog.value).JestAssertion<any>.toEqual: <number[]>(expected: number[]) => voidtoEqual([0]);
let count: numbercount = 1;
function flushSync(fn?: (() => void) | undefined): voidSynchronously 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: anylog.value).JestAssertion<any>.toEqual: <number[]>(expected: number[]) => voidtoEqual([0, 1]);
});
const cleanup: () => voidcleanup();
});コンポーネントテスト
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[] | 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
UserConfig.resolve?: (ResolveOptions & {
alias?: AliasOptions;
}) | undefined
Configure resolver
resolve: 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
? {
ResolveOptions.conditions?: string[] | undefinedconditions: ['browser']
}
: var undefinedundefined
});その後、テストするコンポーネントをインポートし、プログラムで対話して、結果に関する期待を記述するテストファイルを作成できます
import { function flushSync(fn?: (() => void) | undefined): voidSynchronously 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>): 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>): voidUnmounts a component that was previously mounted using mount or hydrate.
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 | 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: 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.InnerHTML.innerHTML: stringinnerHTML).JestAssertion<string>.toBe: <string>(expected: string) => voidtoBe('<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();
function flushSync(fn?: (() => void) | undefined): voidSynchronously 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: Documentdocument.Document.body: HTMLElementSpecifies the beginning and end of the document body.
body.InnerHTML.innerHTML: stringinnerHTML).JestAssertion<string>.toBe: <string>(expected: string) => voidtoBe('<button>1</button>');
// Remove the component from the DOM
function unmount(component: Record<string, any>): voidUnmounts 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: 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 | 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: 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("/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: LegacyComponentTypeComponent);
const const button: HTMLElementbutton = 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: 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);
});双方向バインディング、コンテキスト、またはスニペット props を含むコンポーネントテストを作成する場合は、特定のテスト用のラッパーコンポーネントを作成して、それと対話するのが最善です。`@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: 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 と対話し、アサーションを記述します。
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();
});