测试有助于您编写和维护代码,并防止回归。测试框架可以帮助您完成这些任务,允许您描述关于代码应该如何行为的断言或期望。Svelte 对您使用的测试框架没有意见——您可以使用像 [Vitest](https://vitest.dev/)、[Jasmine](https://jasmine.github.io/)、[Cypress](https://www.cypress.io/) 和 [Playwright](https://playwright.dev/) 这样的解决方案编写单元测试、集成测试和端到端测试。
##
单元和集成测试使用 Vitest
单元测试允许您测试代码的独立小部分。集成测试允许您测试应用程序的某些部分,以查看它们是否可以协同工作。如果您使用 Vite(包括通过 SvelteKit),我们建议使用[Vitest](https://vitest.dev/)。您可以使用 Svelte CLI 在项目创建期间或之后[设置 Vitest](/docs/cli/vitest)。
设置 Vitest 的手动步骤,首先安装它:
```bash
npm install -D vitest
```
然后调整你的 `vite.config.js`:
```js
/// file: vite.config.js
import { defineConfig } from +++'vitest/config'+++;
export default defineConfig({
// ...
// Tell Vitest to use the `browser` entry points in `package.json` files, even though it's running in Node
resolve: process.env.VITEST
? {
conditions: ['browser']
}
: undefined
});
```
> \[!注意\] 如果不希望加载所有包的浏览器版本,因为(例如)您还测试后端库,[您可能需要求助于别名配置](https://github.com/testing-library/svelte-testing-library/issues/222#issuecomment-1909993331)
您现在可以为您的`.js/.ts`文件中的代码编写单元测试了:
```js
/// file: multiplier.svelte.test.js
import { flushSync } from 'svelte';
import { expect, test } from 'vitest';
import { multiplier } from './multiplier.svelte.js';
test('Multiplier', () => {
let double = multiplier(0, 2);
expect(double.value).toEqual(0);
double.set(5);
expect(double.value).toEqual(10);
});
```
```js
/// file: multiplier.svelte.js
/**
* @param {number} initial
* @param {number} k
*/
export function multiplier(initial, k) {
let count = $state(initial);
return {
get value() {
return count * k;
},
/** @param {number} c */
set: (c) => {
count = c;
}
};
}
```
###
使用您的测试文件中的符文
由于 Vitest 以与您的源文件相同的方式处理测试文件,只要文件名包含`.svelte`,您就可以在测试中使用 runes。
```js
/// file: multiplier.svelte.test.js
import { flushSync } from 'svelte';
import { expect, test } from 'vitest';
import { multiplier } from './multiplier.svelte.js';
test('Multiplier', () => {
let count = $state(0);
let double = multiplier(() => count, 2);
expect(double.value).toEqual(0);
count = 5;
expect(double.value).toEqual(10);
});
```
```js
/// file: multiplier.svelte.js
/**
* @param {() => number} getCount
* @param {number} k
*/
export function multiplier(getCount, k) {
return {
get value() {
return getCount() * k;
}
};
}
```
如果被测试的代码使用了效果,您需要将测试包裹在`$effect.root`内:
```js
/// file: logger.svelte.test.js
import { flushSync } from 'svelte';
import { expect, test } from 'vitest';
import { logger } from './logger.svelte.js';
test('Effect', () => {
const cleanup = $effect.root(() => {
let count = $state(0);
// logger uses an $effect to log updates of its input
let log = logger(() => count);
// effects normally run after a microtask,
// use flushSync to execute all pending effects synchronously
flushSync();
expect(log).toEqual([0]);
count = 1;
flushSync();
expect(log).toEqual([0, 1]);
});
cleanup();
});
```
```js
/// file: logger.svelte.js
/**
* @param {() => any} getValue
*/
export function logger(getValue) {
/** @type {any[]} */
let log = [];
$effect(() => {
log.push(getValue());
});
return log;
}
```
###
组件测试
可以使用 Vitest 单独测试您的组件。
> \[注意\] 在编写组件测试之前,考虑你是否真的需要测试该组件,或者这更多的是关于组件内部的逻辑 *内部*。如果是这样,考虑提取出该逻辑以单独测试,而无需组件的额外开销。
要开始,安装 jsdom(一个模拟 DOM API 的库):
```bash
npm install -D jsdom
```
然后调整你的 `vite.config.js`:
```js
/// file: vite.config.js
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [
/* ... */
],
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.
environment: 'jsdom'
},
// Tell Vitest to use the `browser` entry points in `package.json` files, even though it's running in Node
resolve: process.env.VITEST
? {
conditions: ['browser']
}
: undefined
});
```
之后,您可以创建一个测试文件,在其中导入组件进行测试,以编程方式与之交互,并编写关于结果的预期:
```js
/// file: component.test.js
import { flushSync, mount, unmount } from 'svelte';
import { expect, test } from 'vitest';
import Component from './Component.svelte';
test('Component', () => {
// Instantiate the component using Svelte's `mount` API
const component = mount(Component, {
target: document.body, // `document` exists because of jsdom
props: { initial: 0 }
});
expect(document.body.innerHTML).toBe('');
// Click the button, then flush the changes so you can synchronously write expectations
document.body.querySelector('button').click();
flushSync();
expect(document.body.innerHTML).toBe('');
// Remove the component from the DOM
unmount(component);
});
```
虽然这个过程非常直接,但它也是低级且有些脆弱的,因为您的组件的精确结构可能会频繁变化。像[@testing-library/svelte](https://testing-library.com/docs/svelte-testing-library/intro/)这样的工具可以帮助简化您的测试。上述测试可以重写如下:
```js
/// file: component.test.js
import { render, screen } from '@testing-library/svelte';
import userEvent from '@testing-library/user-event';
import { expect, test } from 'vitest';
import Component from './Component.svelte';
test('Component', async () => {
const user = userEvent.setup();
render(Component);
const button = screen.getByRole('button');
expect(button).toHaveTextContent(0);
await user.click(button);
expect(button).toHaveTextContent(1);
});
```
在编写涉及双向绑定、上下文或片段属性的组件测试时,最好为您的特定测试创建一个包装组件并与它交互。`@testing-library/svelte` 包含一些 [示例](https://testing-library.com/docs/svelte-testing-library/example)。
##
端到端测试使用 Playwright
E2E(代表“端到端”)测试允许您从用户的角度测试您的完整应用程序。本节以[Playwright](https://playwright.dev/)为例,但您也可以使用其他解决方案,如[Cypress](https://www.cypress.io/)或[NightwatchJS](https://nightwatchjs.org/)。
您可以使用 Svelte CLI 在项目创建期间或之后设置 Playwright。您还可以使用`npm init playwright`进行设置。此外,您可能还想安装一个 IDE 插件,例如[VS Code 扩展,以便在 IDE 内部执行测试。](https://playwright.dev/docs/getting-started-vscode)
如果您已运行`npm init playwright`或未使用 Vite,您可能需要调整 Playwright 配置来告诉 Playwright 在运行测试之前要做什么 - 主要是在某个端口启动您的应用程序。例如:
```js
/// file: playwright.config.js
const config = {
webServer: {
command: 'npm run build && npm run preview',
port: 4173
},
testDir: 'tests',
testMatch: /(.+\.)?(test|spec)\.[jt]s/
};
export default config;
```
您现在可以开始编写测试。这些测试完全不了解 Svelte 作为框架,所以您主要与 DOM 交互并编写断言。
```js
// @errors: 2307 7031
/// file: tests/hello-world.spec.js
import { expect, test } from '@playwright/test';
test('home page has expected h1', async ({ page }) => {
await page.goto('/');
await expect(page.locator('h1')).toBeVisible();
});
```