📙

Svelteのユニットテスト事情 (2021年12月版)

2021/12/17に公開

はじめに

この記事は Svelte Advent Calendar 2021 の 17 日目の記事です。
筆者は仕事の関係もあり、最近本格的に React/Vue でユニットテストを書くようになりました(えっ今更すぎるだって?色々訳がありまして…)。その流れで、そういえば Svelte のユニットテスト事情どうなっているのだろうと考えるようになり、書き方の勉強も兼ねて本記事を書くことにしました。

Svelte のユニットテストを書く方法

https://svelte.dev/faq#how-do-i-test-svelte-apps

公式 Q&A の「How do I test Svelte apps?」には次の方法が紹介されています。

ただし、後述する Svelte Society の公式レシピなど前者の方法で紹介されることが殆どで、Testing Library を使えば問題ないです(実際、Svelte のコアメンバーが Svelte Testing Library の公式リポジトリにコントリビュートしてたりする)。

Svelte Testing Library について

React のテストを書いてる人にはお馴染み(だと思いたい)Testing Libraryの Svelte 版になります。正直なところ、その認識だけで問題ありません。

書き方

基本的な書き方は次のサイトを見れば問題ないです。Context API やコンポーネントで持っている情報の取得/叩き方がちょっと特殊なくらいです。
Svelte 自体、React 以上に外部ライブラリの選択肢が幅広いため、ちょっと特殊な書き方をしたくなったときはReact Testing Libraryも参考にしながら書いていけば、あまり迷うことなく書けるかと。

https://testing-library.com/docs/svelte-testing-library/example/
https://sveltesociety.dev/recipes/testing-and-debugging/unit-testing-svelte-component

開発環境

PC: MacBook Pro(Apple M1, 2020)
OS: macOS Monterey 12.0.1

環境構築

ここが一番の鬼門です(特に TypeScript で書くとなる場合)。残念ながら、React での create-react-app、Vue の Vue CLI のような形で、Svelte では公式から楽に設定してくれる方法は現状提供されていません…。一応もう少し楽に設定する方法はあるため、今回は Svelte Testing Library 公式で書かれている手順と、有志作成のリポジトリを使って楽めに設定する方法の 2 つを紹介します。

公式に沿った手順

基本的には、Svelte Testing Library の Setup ページと svelte-jester の README を参考にしました。

https://testing-library.com/docs/svelte-testing-library/setup
https://github.com/mihar-22/svelte-jester

ただしこれだと複数ライブラリのドキュメントを横断しなければいけず、わかりにくかったため、こちらの記事も参考にしました。
babeljest の設定、 svelte-preprocess 周り本当に弱いので勉強しなければ…)
https://daveceddia.com/svelte-typescript-jest/

1. Svelte + TypeScript のテンプレートを生成

今回は svelte-typescript-test というプロジェクト名で作成するので、npx degit コマンドでこちらの名称を指定し、作成します。

npx degit sveltejs/template svelte-typescript-test

cd svelte-typescript-test
node scripts/setupTypeScript.js
yarn install # OR npm install

2. ユニットテスト書くのに必要なライブラリのインストール

インストールする必要があるライブラリは以下となります。「えっ多くない…」ってなりそうですが、マニュアルインストールする以上、仕方ないかと。

ライブラリ名の頭について
  • TypeScript を使う場合のみ必要になるライブラリには、 ☆ がついています
  • TypeScript のみでテストする場合、インストールする必要がないライブラリには、 △ がついています
- jest : ご存知フロントのユニットテストツール
- ☆ ts-jest : TypeScriptでJestを使えるようにしてくれるライブラリ
- @testing-library/svelte : Svelte Testing Library本体
- @testing-library/jest-dom : toBeInTheDocumentなどDOM要素関係のTesting Library
- svelte-jester : .svelteファイルでJestを使えるようにしてくれるライブラリ
- ☆ @types/jest : Jestの型定義
- △ babel-jest : JavaScriptでJestを使えるようにしてくれるライブラリ
- △ @babel/preset-env : babel-jestとセットで使うライブラリ

それではインストールしていきます。今回は vanilla js でテストコードを書く予定はないので、babel-jest/@babel/preset-env についてはインストールしません。

yarn add -D \
    jest ts-jest \
    @testing-library/svelte @testing-library/jest-dom \
    svelte-jester @types/jest

# OR
# npm install --save-dev ...

3. 設定ファイルの作成

まず package.json に Jest 用の script を追加しておきます。
--watch をつけておくと、Jest がファイルの変更を検知してくれるので便利です。

package.json
{
  "scripts": {
    ...
+   "test": "jest",
+   "test:watch": "npm test --watch"
  }
}

package.json にさらに追加します。
babel-jest はインストールしていないので、transform の中に指定する必要はありません。一方、moduleFileExtensions には必ず js を指定しないといけないため、入れておきます。さらに @testing-library/jest-dom を使う関係で、testEnvironment も指定しておきます。

※ここは package.json ではなく、プロジェクト直下に jest.config.js を作成して、"transform": { 以降の内容を追記しても大丈夫かと。

package.json
{
  "scripts": {
    ...
+   "jest": {
+     "transform": {
+       "^.+\\.svelte$": [
+         "svelte-jester",
+         {
+           "preprocess": true
+         }
+       ],
+       "^.+\\.ts$": "ts-jest"
+     },
+     "moduleFileExtensions": [
+       "js",
+       "ts",
+       "svelte"
+     ],
+     "testEnvironment": "jsdom"
+   }
  }
}

package.json への追記はここまでです。
次は svelte.config.js をプロジェクト直下に新規で作成します。svelte-preprocess は Svelte アプリの作成した際に、一緒にインストールされるため、気にする必要はありません。

TypeScript だけでテスト実行する場合、作成しなくても通ったという情報もありましたが、こちらの環境だと、ないとテストが実行できませんでした…。

svelte.config.js
+  const sveltePreprocess = require("svelte-preprocess");
+
+  module.exports = {
+    preprocess: sveltePreprocess(),
+  };

最後に tsconfig.json へ 3 行追記します。
インストールした @types/jest をここに指定することで Jest の型定義がグローバルに有効化されます。

tsconfig.json
{
   ...
+  "compilerOptions": {
+   "types": ["jest"]
+  }
}

設定は以上です。

4. Svelte ファイルのテストを書く

ようやく本題です。

記事で作成するプロジェクトコードは以下のページに設置しています。

https://github.com/miily8310s/svelte-testing-examples/tree/master/svelte-typescript-test

今回の記事では文字数の都合上、Svelte コンポーネントの基礎的な書き方のみを取り上げ、Jest の書き方・メソッドに関しての説明は省略します。

もう少し本格的なものがみたい方は、 Todo アプリのテストコードも置いたので、併せて見てみてください。
https://github.com/miily8310s/svelte-testing-examples/tree/master/svelte-todo-test

まずは「1. Svelte + TypeScript のテンプレートを生成」で生成された App.svelte にある name という props に適当な文字列を渡したらテンプレートに表示されるという、一番シンプルな例を挙げてみます。

App.svelte
<script lang="ts">
  export let name: string;
</script>

<main>
  <h1>Hello {name}!</h1>
  <p>Visit the <a href="https://svelte.dev/tutorial">Svelte tutorial</a> to learn how to build Svelte apps.</p>
</main>
App.spec.ts
import "@testing-library/jest-dom";
import { render } from "@testing-library/svelte";
import App from "../App.svelte";

// h1タグの文字列が「Hello test!!!!」と一致していることを確認
test("should render App component", () => {
  const { getByRole } = render(App, {
    name: "test!!!",
  });
  expect(getByRole("heading").textContent).toBe("Hello test!!!!");
});

もう 1 つ今回挙げる例として、ボタンをクリックすると合計値が変化する例も紹介します。
sum の中の数値がリアクティブに変化することを確認するテストになります。tsconfig.json

Button.svelte
<script lang="ts">
  let numbers = [0, 1, 2, 3]
  const addNumber = () => {
    numbers = [...numbers, numbers.length]
  }
  $: sum = numbers.reduce((previous, current) => previous + current, 0)
</script>

<button on:click={addNumber}>
  Add number
</button>

<p>sum: {sum}</p>
Button.spec.ts
import { fireEvent } from "@testing-library/dom";
import "@testing-library/jest-dom";
import { render } from "@testing-library/svelte";
import Button from "../components/Button.svelte";

test("should fire onclick event", async () => {
  const { getByText } = render(Button);
  const button = getByText("Add number");
  const printSum = getByText("sum: 6");
  // ボタンをクリック(1回目)
  await fireEvent.click(button);
  expect(printSum.textContent).toBe("sum: 10");
  // ボタンをクリック(2回目)
  await fireEvent.click(button);
  expect(printSum.textContent).toBe("sum: 15");
});

有志作成のライブラリ/テンプレートを使って、設定する方法

マニュアルインストールしなくても Svelte Testing Library を使う方法は一応存在します。

テンプレートを使う方法

Svelte 公式ではテンプレート一覧が記載されており、以下のサイトから探すことが出来ます。
ただし最終更新が 2019 年だったりで、これから新規で Svelte プロジェクトを作成する際に使用するのはちょっと厳しい気がします。

https://sveltesociety.dev/templates/

また、環境構築の節で参考にした記事として名前が出ていた Dave Ceddia 氏もテンプレートを載せています(更新が1年前から止まっているのが残念ですが…)。
https://github.com/dceddia/svelte-typescript-jest

そろそろアクティブに更新されるテンプレートが出てほしいところです自分で作れって話ですが

ライブラリを使う方法

現状一番安定しているのは「Svelte Add を使う方法」な気がします。
前述のテンプレート一覧ページと同じページに記載されているなど、実質公式公認のプロジェクトとなっています。
掲載されているライブラリを指定することで、簡単に SvelteKit アプリへ追加できます。

https://github.com/svelte-add/svelte-add

有志作成のものもあり、以下のライブラリを使用することによって Jest を簡単に追加することが出来ます。TS 化も可能です。

https://github.com/rossyman/svelte-add-jest

本記事の最後は svelte-add-jest で生成する手順の記載をもって、締めます。

svelte-add-jest の適用手順

本節で作成したアプリのリポジトリページはこちらです。
https://github.com/miily8310s/svelte-testing-examples/tree/master/svelte-kit-test

まずは SvelteKit アプリを新規作成します。
色々質問されますが、すべて Yes を選択して OK です(TypeScript の有効化も忘れずに!)。

npm init svelte@next 作成するアプリ名

svelte-add-jest を適用するため、README にかかれている通り、
tsconfig.json のコメントアウトされている箇所をすべて削除しておく必要があります。
しないと、svelte-add-jest の適用に失敗します。

それでは svelte-add-jest を適用していきます。TypeScript 用設定はデフォルトで無効になっているため、--ts フラグを付ける必要があります。
また、こちらの環境ではつけないとインストールに失敗したため、--no-ssh フラグも併せてつけておきます。

npx apply rossyman/svelte-add-jest --ts --no-ssh

これで npm run testyarn test)を叩くと一旦ユニットテストが通ります。

ただしここで1点問題が発生します。
次のテストコードが生成されるのですが、describe / testexpect の型定義が不明になっており、ts の型エラーが発生します。npm i --save-dev @types/jest を叩いてねと出てくるのですが、package.json には存在しており、インストール済みの状態でした。

src/routes/index-dom.spec.ts
describe('Index', () => {
  let renderedComponent: RenderResult;

  beforeEach(() => {
    renderedComponent = render(Index);
  });
  describe('once the component has been rendered', () => {
    test('should show the proper heading', () => {
      expect(renderedComponent.getByText(/SvelteKit/)).toBeInTheDocument();
    });
  });
});

型定義が不明なままでも、テストは通るのですが、上記で挙げている Jest の関数がコード補完で呼び出せないため、かなり書きづらくなってしまいます。

原因を調べたところ、次の2点が原因でした。

  • tsconfig.json を確認したところ、compilerOptions typesjest が定義されておらず、Jest の型定義がグローバルに有効化されていない。
  • src/**/*.spec.ts に該当するテストファイルが exclude されていて、tsconfig が読み込まれていなかった。

今回とった解決策は以下の通りです。

まず、生成される tsconfig.spec.json のコードを削除しました(ファイル削除すると、動かなくなる可能性があったため、今回は一旦コードの削除で留めました)。

tsconfig.spec.json
- {
-  	"extends": "./tsconfig.json",
-  	"compilerOptions": {
-  		"isolatedModules": false
-  	},
-  	"exclude": ["src/**"],
-  	"include": ["src/**/*.spec.ts"]
- }

コード削除出来たら、次は tsconfig.json に Jest の型定義を読み込ませます。

tsconfig.json
{
   ...
+  "compilerOptions": {
+   "types": ["jest"]
+  }
   // @types/jestをグローバルに適用させるため、excludeさせない
-  "exclude": ["src/**/*.spec.ts"]
}

これでもう一度 yarn test or npm run test するとテストが通るはずです。

Jest の型定義が読み取れない問題に関しては、こちらの質問が詳しいです。
https://stackoverflow.com/questions/54139158/cannot-find-name-describe-do-you-need-to-install-type-definitions-for-a-test

本記事は以上となります。状況が変わり次第、またどこかにまとめる予定です。

補足として Svelte のユニットテストを書く上で、参考になるページを一覧でまとめておきます。参考になれば幸いです(文中で登場した記事リンクも一部ここで再掲しています)。

【補足】Svelte のユニットテストを書く上で、参考になるページ

GitHubで編集を提案

Discussion