Vitest / Playwrightを使ってSvelteのコンポーネントをテストする

[Ateam Group U-30 Advent Calendar 2022](https://qiita.com/advent-calendar/2022/ateam-group-u30)の1日目は
株式会社エイチームライフデザインの@oekazumaが担当します。

# はじめに
[Svelte](https://svelte.jp/)、[SvelteKit](https://kit.svelte.jp/)のプロジェクトにおいて、コンポーネントテストを書きたい場合、どのようなテストランナーを使ってどう書けばいいかを解説していきます。
SvelteKitが用意してくれているプロジェクト作成のCLIでは[Playwright](https://playwright.dev/)と[Vitest](https://vitest.dev/)がデフォルトで追加できるようになっていますのでこれらのテストランナーを使ったコンポーネントテストを紹介します。

今回、作ったサンプルはこの[リポジトリ](https://github.com/oekazuma/svelte-component-test-vitest-playwright)で確認できます。

# SvelteKitとは
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217487/b438206a-d409-6814-dfa0-35862c596c7e.png" width="350">

SvelteをベースとしたハイパフォーマンスなWebアプリを作るためのフレームワークです。
ReactでいうNext.js、VueでいうNuxt.jsような立ち位置だと認識していただければわかりやすいかと思います。
2022/12/1 現在の開発ステータスは1.0のRC版がリリースされている状態で、正式な1.0がリリースされる日も近いでしょう。

# Vitestとは
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217487/2a786341-53f1-27aa-d66e-8e1094be56ad.png" width="200">

Vite環境で動作する高速なテストフレームワークで、Jestの代替として期待されているフレームワークです。
SvelteKitはViteを使用しているので簡単な設定のみで利用可能になり、相性が抜群です。

# Playwrightとは
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217487/ba9427ab-4ef8-d948-3307-e20c532d37b3.png" width="200">

Microsoftが開発しているE2Eテストランナーです。対応しているブラウザ数が多かったり、実行速度が他のE2Eテストランナーと比べて速いことが特徴で、Svelteコミュニティで推奨されています。

# 環境
- Node 
    - v18.12.0
- npm 
    - v8.19.2
- @sveltejs/kit 
    - v1.0.0-next.567
- svelte 
    - v3.53.1
- vitest 
    - v0.25.3
- @testing-library/svelte 
    - v3.2.2
- jsdom 
    - v20.0.3
- @playwright/test 
    - v1.28.1
- @playwright/experimental-ct-svelte
    - v1.28.1

# 新規プロジェクトを作成
SvelteKitの新規プロジェクトをCLIを使って作成します。

```bash
$ npm create svelte@latest svelte-component-test-vitest-playwright
```

今回はTypeScript、Playwright, Vitestを選択してプロジェクトを作成しました。
```bash
✔ Which Svelte app template? › Skeleton project
✔ Add type checking with TypeScript? › Yes, using TypeScript syntax
✔ Add ESLint for code linting? … No
✔ Add Prettier for code formatting? … No
✔ Add Playwright for browser testing? … Yes
✔ Add Vitest for unit testing? … Yes
```

# Vitest + Testing Library
必要となるパッケージを追加でインストールします。

```bash
$ npm install -D @testing-library/svelte jsdom
```

`vite.config.js`と`tsconfig.json`の構成を以下のように変更します。

```diff_javascript:vite.config.js
import { sveltekit } from '@sveltejs/kit/vite';

/** @type {import('vite').UserConfig} */
const config = {
	plugins: [sveltekit()],
	test: {
		include: ['src/**/*.{test,spec}.{js,ts}'],
+		globals: true,
+		environment: 'jsdom'
	}
};

export default config;
```

```diff_json:tsconfig.json
{
	"extends": "./.svelte-kit/tsconfig.json",
	"compilerOptions": {
		"allowJs": true,
		"checkJs": true,
		"esModuleInterop": true,
		"forceConsistentCasingInFileNames": true,
		"resolveJsonModule": true,
		"skipLibCheck": true,
		"sourceMap": true,
		"strict": true,
+		"types": ["vitest/globals"]
	}
}

```

`src/lib`配下にテスト対象のコンポーネントを用意します。 
`+`をクリックすると数字が1増え、`-`をクリックすると数字が1減る、Counterコンポーネントを今回はテストします。

![counter.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217487/95001b2c-4ec1-7e63-3ede-33362fdc0dbf.png)


```html:src/lib/Counter.svelte
<script lang="ts">
	import { spring } from 'svelte/motion';
	let count = 0;
	const displayed_count = spring();
	$: displayed_count.set(count);
	$: offset = modulo($displayed_count, 1);
	function modulo(n: number, m: number) {
		return ((n % m) + m) % m;
	}
</script>

<div class="counter">
	<button on:click={() => (count -= 1)} aria-label="Decrease the counter by one">
		-
	</button>

	<div class="counter-viewport">
		<div class="counter-digits" style="transform: translate(0, {100 * offset}%)">
			<strong class="hidden" aria-hidden="true">{Math.floor($displayed_count + 1)}</strong>
			<strong>{Math.floor($displayed_count)}</strong>
		</div>
	</div>

	<button on:click={() => (count += 1)} aria-label="Increase the counter by one">
		+
	</button>
</div>

<style>
	.counter {
		display: flex;
		border-top: 1px solid rgba(0, 0, 0, 0.1);
		border-bottom: 1px solid rgba(0, 0, 0, 0.1);
		margin: 1rem 0;
	}
	.counter button {
		width: 2em;
		padding: 0;
		display: flex;
		align-items: center;
		justify-content: center;
		border: 0;
		background-color: transparent;
		touch-action: manipulation;
		color: var(--text-color);
		font-size: 2rem;
	}
	.counter button:hover {
		background-color: var(--secondary-color);
	}
	.counter-viewport {
		width: 8em;
		height: 4em;
		overflow: hidden;
		text-align: center;
		position: relative;
	}
	.counter-viewport strong {
		position: absolute;
		display: flex;
		width: 100%;
		height: 100%;
		font-weight: 400;
		color: var(--accent-color);
		font-size: 4rem;
		align-items: center;
		justify-content: center;
	}
	.counter-digits {
		position: absolute;
		width: 100%;
		height: 100%;
	}
	.hidden {
		top: -100%;
		user-select: none;
	}
</style>
```

`src/lib`配下にテストコードを追加します。

```ts:src/lib/Counter.test.ts
import { render, fireEvent, screen } from '@testing-library/svelte';
import Counter from './Counter.svelte';

describe('Counter.svelte', async () => {
	test('カウンターの初期値は0', async () => {
		render(Counter);
		expect(screen.getByText('0')).toBeTruthy();
	});
	test('減算処理', async () => {
		render(Counter);
		const decreaseButton = screen.getByLabelText('Decrease the counter by one');
		await fireEvent.click(decreaseButton);
		await fireEvent.click(decreaseButton);
		const counter = await screen.findByText('-2');
		expect(counter).toBeTruthy();
	});
	test('加算処理', async () => {
		render(Counter);
		const increaseButton = screen.getByLabelText('Increase the counter by one');
		await fireEvent.click(increaseButton);
		const counter = await screen.findByText('1');
		expect(counter).toBeTruthy();
	});
});
```

`package.json`にテストスクリプトを追加します。
今回はvitestとplaywrightを両方使うのであえてわかりやすい名前にしています。

```diff_json:package.json
{
	"name": "svelte-component-test-vitest-playwright",
	"version": "0.0.1",
	"private": true,
	"scripts": {
		"dev": "vite dev",
		"build": "vite build",
		"preview": "vite preview",
-		"test": "playwright test"
		"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
		"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
-       "test:unit": "vitest",
+		"test:vitest": "vitest",
+		"test:playwright": "playwright test"
	},
	"devDependencies": {
		"@playwright/test": "^1.28.1",
		"@sveltejs/adapter-auto": "^1.0.0-next.90",
		"@sveltejs/kit": "^1.0.0-next.567",
		"@testing-library/svelte": "^3.2.2",
		"jsdom": "^20.0.3",
		"svelte": "^3.53.1",
		"svelte-check": "^2.9.2",
		"svelte-preprocess": "^4.10.7",
		"tslib": "^2.4.1",
		"typescript": "^4.9.3",
		"vite": "^3.2.4",
		"vitest": "^0.25.3"
	},
	"type": "module"
}
```

実際にテストを動かしてみましょう。

```bash
$ npm run test:vitest
```

![vitest.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217487/7f811610-4935-2c53-3c40-a92d2b5dbf13.png)

無事動きました:tada:

# Playwright Components Tests
Playwrightはコンポーネントテストを実験的にサポートしています。 
必要となるパッケージがあるので追加でインストールします。

```bash
$ npm install -D @playwright/experimental-ct-svelte
```

`playwright-ct.config.ts`を作成します。

```ts:playwright-ct.config.ts
import type { PlaywrightTestConfig } from '@playwright/experimental-ct-svelte';
import { resolve } from 'node:path';

const config: PlaywrightTestConfig = {
	testDir: 'tests/component',
	use: {
		ctViteConfig: {
			resolve: {
				alias: {
					$lib: resolve('src/lib')
				}
			}
		}
	}
};

export default config;
```

実行に必要なファイルを追加します。

```html:playwright/index.html
<html lang="en">
	<body>
		<div id="root"></div>
		<script type="module" src="/playwright/index.ts"></script>
	</body>
</html>
```

```ts:playwright/index.ts
// 例えば、app.cssのような、コンポーネントの実行時に必要なファイルをimportします。 
// 今回は不要なのでファイルのみ作成してコメントアウトしています。
// import '../src/app.css';
```

テストコードを追加します。

```tests/component/Counter.test.ts
import { test, expect } from '@playwright/experimental-ct-svelte';
import Counter from '$lib/Counter.svelte';

test('Counter.svelte', async ({ mount }) => {
    const component = await mount(Counter);
    await expect(component).toContainText("0");
    await component.locator('[aria-label="Decrease the counter by one"]').dblclick();
    await expect(component).toContainText('-2');
    await component.locator('[aria-label="Increase the counter by one"]').click();
    await expect(component).toContainText('-1');
});
```

`package.json`にテストスクリプトを追加します。
今回はE2Eテストについては特には触れませんが、E2Eテストのスクリプトと分けておくと便利です。

```diff_json:package.json
{
	"name": "svelte-component-test-vitest-playwright",
	"version": "0.0.1",
	"private": true,
	"scripts": {
		"dev": "vite dev",
		"build": "vite build",
		"preview": "vite preview",
		"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
		"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
		"test:vitest": "vitest",
+       "test:playwright-ct": "playwright test -c playwright-ct.config.ts",
-		"test:playwright": "playwright test",
+		"test:playwright-e2e": "playwright test",
	},
	"devDependencies": {
		"@playwright/experimental-ct-svelte": "^1.28.1",
		"@playwright/test": "^1.28.1",
		"@sveltejs/adapter-auto": "^1.0.0-next.90",
		"@sveltejs/kit": "^1.0.0-next.567",
		"@testing-library/svelte": "^3.2.2",
		"jsdom": "^20.0.3",
		"svelte": "^3.53.1",
		"svelte-check": "^2.9.2",
		"svelte-preprocess": "^4.10.7",
		"tslib": "^2.4.1",
		"typescript": "^4.9.3",
		"vite": "^3.2.4",
		"vitest": "^0.25.3"
	},
	"type": "module"
}
```


実際にテストを動かしてみましょう。

```bash
$ npm run test:playwright-ct
```

![playwright.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217487/bd4e6a7e-2eb3-b783-d7dd-b86cf3b1a306.png)

無事動きました:tada:

# まとめ
## Vitest + Testing Library 
- メリット
    - 実行速度がPlaywrightに比べて速い。
    - Testing Libraryは採用事例も多く、安定している。

- デメリット
    - セットアップや依存関係がやや複雑。
    - Vitestの構文、Testing Libraryの構文の両方を学ばないといけない。
  
## Playwright Components Tests
- メリット
    - ブラウザ上で実行するのでより本番環境に近い状態でテストができる。
    - Playwrightの構文が使えるので追加で学ぶことが少ない。

- デメリット
    - 実行速度がVitest + Testing Libraryに比べて遅い。
    - まだまだ実験段階で不安定である。

## どちらを選ぶべきか
個人的には現時点では、Vitest + Testing Libraryを選ぶほうがいいと思います。
コンポーネントテストをどこまで重視するかにもよりますが、Vitest + Testing Libraryの構成は安定していますし、Playwrightに比べて実行速度も速いので開発体験がいいです。
現状、僕がSvelteKitプロジェクトでどの領域のテストに対してどのテストランナーを選ぶか問われると下記のように回答します。

| ユニットテスト | コンポーネントテスト | E2Eテスト |
|:-----------:|:------------:|:------------:|
| Vitest      | Vitest + Testing Library        | Playwright         |

コンポーネントテストの箇所はPlaywright、StoryBookなどの選択肢もあると思いますし、何を担保したいかによって変わると思いますので状況に応じて最適な状態を模索していきたいですね。