Svelte 测试指南

测试帮助编写和维护代码,防止回归。测试框架让你描述代码行为应满足的断言与预期。Svelte 不限定框架,可以用 Vitest、Jasmine、Cypress、Playwright 等编写单元、集成和端到端测试。

使用 Vitest 进行单元与组件测试

单元测试检查小而独立的代码片段,集成测试检查应用各部分能否协同工作。如果使用 Vite,包括 SvelteKit,推荐 Vitest。可在创建项目时或之后通过 Svelte CLI 配置。

手动安装:

npm install -D vitest

修改 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
});

如果不希望全部包都加载浏览器版本,例如还需要测试后端库,可能需要改用 alias 配置。

现在可以为 .js/.ts 中的代码编写测试。multiplier.svelte.test:

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);
});

multiplier.svelte 的 JavaScript 版本:

/**
 * @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;
		}
	};
}

TypeScript 版本:

export function multiplier(initial: number, k: number) {
	let count = $state(initial);

	return {
		get value() {
			return count * k;
		},

		set: (c: number) => {
			count = c;
		}
	};
}

在测试文件中使用 rune

Vitest 与源文件一样处理测试文件,因此文件名包含 .svelte 时,可以在测试中使用 rune:

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);
});

相应的 JavaScript 实现:

/**
 * @param {() => number} getCount
 * @param {number} k
 */
export function multiplier(getCount, k) {
	return {
		get value() {
			return getCount() * k;
		}
	};
}

TypeScript 实现:

export function multiplier(getCount: () => number, k: number) {
	return {
		get value() {
			return getCount() * k;
		}
	};
}

如果被测代码使用 effect,需要用 $effect.root 包围测试。logger.svelte.test:

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();
});

logger 的 JavaScript 实现:

/**
 * @param {() => any} getValue
 */
export function logger(getValue) {
	/** @type {any[]} */
	let log = [];

	$effect(() => {
		log.push(getValue());
	});

	return log;
}

TypeScript 实现:

export function logger(getValue: () => any) {
	let log: any[] = [];

	$effect(() => {
		log.push(getValue());
	});

	return log;
}

组件测试

可以单独渲染组件,在真实或模拟浏览器中模拟行为、执行断言,无需启动整个应用。

编写前,先想清楚需要测试的是组件本身,还是其中的逻辑。如果主要是逻辑,可以考虑提取后独立测试,避免组件开销。

先安装模拟 DOM API 的 jsdom:

npm install -D jsdom

修改 vite.config.js:

import { defineConfig } from 'vitest/config';

export default defineConfig({
	plugins: [
		/* ... */
	],
	test: {
		// If you are testing components client-side, you need to set up 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
});

然后创建测试文件,导入组件,程序化交互,并断言结果:

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('<button>0</button>');

	// Click the button, then flush the changes so you can synchronously write expectations
	document.body.querySelector('button')?.click();
	flushSync();

	expect(document.body.innerHTML).toBe('<button>1</button>');

	// Remove the component from the DOM
	unmount(component);
});

流程直接,但较底层,也较脆弱,因为组件的精确结构可能经常变化。@testing-library/svelte 可以简化测试,前面的例子可改写为:

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);
});

涉及双向绑定、context 或 snippet props 时,最好为具体测试创建包装组件,再与其交互。@testing-library/svelte 文档提供相关示例。

使用 Storybook 测试组件

Storybook 用于开发和记录 UI 组件,也可以测试组件。它通过 Vitest 浏览器模式,在真实浏览器渲染,提供更接近实际的环境。

使用 Svelte CLI 的 npx sv add storybook 安装,并选择包含测试功能的推荐配置。如果已经使用 Storybook,或需要更多细节,参阅 Storybook 测试文档。

可以为组件不同状态创建 stories,并使用 play 函数,通过 Testing Library 和 Vitest API 模拟交互、编写断言。下面两个 story 分别显示空 LoginForm,以及模拟用户填写表单:

<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 进行端到端测试

E2E 测试从用户视角检查完整应用。本节以 Playwright 为例,也可选择 Cypress 或 NightwatchJS。

可以在创建项目时或之后通过 Svelte CLI 配置,也可运行 npm init playwright。安装 VS Code 等 IDE 扩展,还能直接从编辑器执行测试。

如果使用了 npm init playwright,或没有使用 Vite,可能需要修改配置,告诉 Playwright 测试前如何启动应用及使用哪个端口。例如:

const config = {
	webServer: {
		command: 'npm run build && npm run preview',
		port: 4173
	},
	testDir: 'tests',
	testMatch: /(.+\.)?(test|spec)\.[jt]s/
};

export default config;

现在可以编写测试。这些测试不需要了解 Svelte 框架,主要与 DOM 交互并执行断言。tests/hello-world.spec:

import { expect, test } from '@playwright/test';

test('home page has expected h1', async ({ page }) => {
	await page.goto('/');
	await expect(page.locator('h1')).toBeVisible();
});

原文:Testing。作者/维护方:Svelte 文档维护者。本文为中文翻译,代码及命令保留原文。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容