TS×React Vitestで単体テストを導入する 【環境構築 編】

# 投稿目的
・備忘録
・よりよい方法をご教示いただきたい



# はじめに
先日、[TS×React×Express×Firebase×Render でアプリケーションを作りました](https://qiita.com/ko-sato-primebrains/items/0215cd9c54e85805aeb8) という記事を投稿させていただきました。
フロントエンドを構築している際、「せっかくだから単体テスト導入したいな~」と思っていましたがうまくいかず...
結局「フロントエンドをローカル起動」 → 「バックエンドをローカル起動 + Firebaseに直接リクエスト」という方法で、アプリケーションの挙動を確かめていました。大変非効率です...

「入力値が6文字未満の場合はボタンが非活性になっているか?」「入力漏れのバリデーションが発動した際、`Alert`上に表示される文言は想定通りになっているか?」などを確かめる場合、アプリケーションを起動してのデバッグよりも単体テスト実施の方がよりスピーディーですし、想定外の事象の見落としを防ぎやすくなります。

※期待値と異なる場合、↓のようにそのことを教えてくれます。
```bash
AssertionError: expected [ …(3) ] to have a length of 4 but got 3
- Expected
+ Received
- 4
+ 3
+ 
 ❯ src/pages/__tests__/sample.test.tsx:47:51
     45|     });
     46|     test("listitemが3つ存在していること", () => {
     47|       expect(testScreen.getAllByRole("listitem")).toHaveLength(4);
       |                                                   ^
     48|     });
     49|     describe("1つめのlistitemについて", () => {

⎯⎯⎯⎯⎯[1/1]⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
 Test Files  1 failed | 11 passed (12)
      Tests  1 failed | 229 passed (230)
   Start at  13:28:16
   Duration  5.14s (transform 1.03s, setup 3.56s, collect 8.16s, tests 6.12s, environment 7.67s, prepare 1.21s)
```

幸いにも業務でReactに触れる機会が多く、先日`Vitest`の環境構築を行いました。
思っていたよりも簡単に、かつ以前試した際よりもテストが記述しやすい環境を構築できたため、備忘も兼ねて単体テストに関する記事を作成いたします。



# 本題
### パッケージ類のインストール
まずは、パッケージ類をインストールしましょう。
`vite`×`React`環境であることを前提としています。

```bash
// テスト関連のパッケージ類
$ npm i -D \
  @testing-library/jest-dom@latest \
  @testing-library/react@latest \
  @testing-library/user-event@latest \
  jsdom@latest \
  unplugin-auto-import@latest \
  vitest@latest

// 通信を伴うmock用(普通にバックエンドのmockとしても使える)
$ npm i -D msw@latest
```

<details><summary> package.json </summary>

```json
  "devDependencies": {
    "@testing-library/jest-dom": "^6.6.3",
    "@testing-library/react": "^16.2.0",
    "@testing-library/user-event": "^14.6.1",
    "jsdom": "^26.0.0",
    "msw": "^2.7.3",
    "unplugin-auto-import": "^19.1.0",
    "vite": "^6.2.0",
    "vitest": "^3.0.7",
    ...
  },
```
</details>

### テスト用のコマンドの用意
`package.json`に`test`コマンドを追加しましょう。

※`bun`を使った場合、`bun test`を実行すると`vitest`ではなく`bun`自体によるテストが実行されてしまいます... (`bun run test`とすれば問題ありませんが)
↑については、`test`→`vitest`などとすることで、`bun vitest`でテストを実行できます。
```json
  "scripts": {
    "test": "vitest --config vitest.config.ts",
    ...
  },
```

### `vitest.config.ts`の用意
`create vite`でアプリケーションを作成すると、`vite.config.ts`が自動生成されると思います。
似たような形式で、`vitest.config.ts`を作成していきます。

<details><summary> vite.config.ts </summary>

※`@tanstack/react-router`環境での`vite.config.ts`を持ってきました。内容はここでは関係ありません。
```TypeScript
import { TanStackRouterVite } from "@tanstack/router-plugin/vite";
import react from "@vitejs/plugin-react-swc";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";

// https://vite.dev/config/
export default defineConfig({
  plugins: [react(), tsconfigPaths(), TanStackRouterVite()],
  server: { host: true },
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ["react", "react-dom"],
          styles: ["@mui/joy", "@emotion/react", "@emotion/styled", "@mui/icons-material"]
        }
      }
    }
  }
});
```
</details>

https://vitest.dev/config/

`vitest`公式にもある通り、既存の`vite.config.ts`に追記することもできますし、今回のように`vitest.config.ts`を作成するなど、別途Configファイルを作成することもできます。

まずは`vitest.config.ts`の内容を以下に示します。
```TypeScript
import AutoImport from "unplugin-auto-import/vite";
import { defineConfig, mergeConfig } from "vitest/config";
import viteConfig from "./vite.config";

export default mergeConfig(
  viteConfig,
  defineConfig({
    test: {
    　// srcディレクトリ配下の「__tests__」ディレクトリ直下の「~.test.{js,ts,jsx,tsx}」をテストファイルとして実行
      include: ["src/**/__tests__/*.test.{js,ts,jsx,tsx}"],
      globals: true,
      environment: "jsdom",
      setupFiles: ["./src/test/setupTests.ts"]
    },
    plugins: [
      AutoImport({
        imports: [
          "vitest",
          // screenは、そのままだとグローバル変数のscreenに見なされるため、testファイル内では「testScreen」というエイリアスで記述する
          { "@testing-library/react": ["render", "waitFor", "act", "renderHook", "cleanup", ["screen", "testScreen"]] },
          // setupTests.tsで実際のimportが実行される。ここが無くてもtestは通るが、型推論が働かないためauto-importを有効化
          { "@testing-library/jest-dom": ["matchers"] },
          { "@testing-library/user-event": ["userEvent"] }
        ],
        dts: "./src/test/auto-import-test.d.ts"
      })
    ]
  })
);
```

>・[`mergeConfig(<デフォルトのConfig>, <追加のConfig>)`](https://vite.dev/guide/api-javascript#mergeconfig)
> → 2つのConfigをマージします。今回は、もともと`vite.config.ts`に定義されているConfigと、本ファイルで定義するConfigをマージするために使います。
>
>・[`test.globals`](https://vitest.dev/config/#globals)
> →いちいち`import`しなくても`vi.mock()`や`render()`を使いたいので`true`に設定します。
>
>・[`test.environment`](https://vitest.dev/config/#environment)
> →componentの状態にように、ブラウザ上でのアプリケーションの動作をテストしたい場合は`jsdom`か`happy-dom`を選択する必要があるようです。
>
>・[`test.setupFiles`](https://vitest.dev/config/#setupfiles)
> →各テストファイルを実行する前に読み込まれるファイルのパスを設定します
>
>・[`unplugin-auto-import`](https://www.npmjs.com/package/unplugin-auto-import)
> →指定したモジュールについて、各ファイルでいちいち`import`をしなくても活用できるようにしたいため (オートインポート)、このパッケージを用います。
> →Config内の`plugins`内で、デフォルトインポートした`AutoImport`を実行し、引数のConfig内ににオートインポートしたい内容を記述していきます。

`unplugin-auto-import`のConfigについて、追記します。

・モジュールすべてをオートインポートしたい場合は、下記のようにモジュール名を`imports`の配列に配置します。
```TypeScript
plugins: [
  AutoImport({
    imports: [ "vitest" ]
  })
]
```

・モジュール内の特定のモジュール(名前付き`export`されているもの)をオートインポートしたい場合は、{ <パッケージ名> : <オートインポートさせたいモジュール名の配列> } を`imports`の配列に配置します。
途中の`["screen", "testScreen"]`については、`screen`というモジュールを`testScreen`というエイリアスでオートインポートできるようになります。

※`@testing-library/react`の`screen`をそのままオートインポートすると、グローバル変数の`screen`と解釈されてしまいエラーが発生したため、エイリアスを設定しています。
```TypeScript
plugins: [
  AutoImport({
    imports: [ { 
      "@testing-library/react": [
        "render", 
        "waitFor", 
        "act", 
        "renderHook", 
        "cleanup", 
        ["screen", "testScreen"]
      ] 
    }]
  })
]
```

・デフォルト`export`されているモジュールをオートインポートしたい場合は、{ <パッケージ名> : [<オートインポートする際のモジュール名>] } を`imports`の配列に配置します。
```TypeScript
plugins: [
  AutoImport({
    imports: [ { "@testing-library/user-event": ["userEvent"] }]
  })
]
```

上記のようにオートインポートの設定を記述し、先ほど定義したテストコマンドを実行 (`npm run test`等) すると、`dts`で定義したパスのファイルにオートインポートを有効化するためのファイルが自動生成されます。

<details><summary>auto-import-test.d.ts</summary>

``` auto-import-test.d.ts
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
  const act: typeof import('@testing-library/react')['act']
  const afterAll: typeof import('vitest')['afterAll']
  const afterEach: typeof import('vitest')['afterEach']
  const assert: typeof import('vitest')['assert']
  const beforeAll: typeof import('vitest')['beforeAll']
  const beforeEach: typeof import('vitest')['beforeEach']
  const chai: typeof import('vitest')['chai']
  const cleanup: typeof import('@testing-library/react')['cleanup']
  const describe: typeof import('vitest')['describe']
  const expect: typeof import('vitest')['expect']
  const it: typeof import('vitest')['it']
  const matchers: typeof import('@testing-library/jest-dom')['matchers']
  const render: typeof import('@testing-library/react')['render']
  const renderHook: typeof import('@testing-library/react')['renderHook']
  const suite: typeof import('vitest')['suite']
  const test: typeof import('vitest')['test']
  const testScreen: typeof import('@testing-library/react')['screen']
  const vi: typeof import('vitest')['vi']
  const vitest: typeof import('vitest')['vitest']
  const waitFor: typeof import('@testing-library/react')['waitFor']
}
```
</details>

### セットアップ用のファイルを作成
先ほどの`vitest.config.ts`作成時、[`test.setupFiles`](https://vitest.dev/config/#setupfiles)を定義しました。
環境構築の最後に、上記で定義したセットアップ用ファイルを整備しましょう。

私の場合は、以下のようになりました。
先ほどのオートインポートが効いているので、`afterEach`・`vi`・`cleanup`はimportしなくても良くなっています!
``` setupTests.ts
import "@testing-library/jest-dom";

afterEach(() => {
　// mockをリセットする
  vi.resetAllMocks();
  vi.clearAllMocks();

  // mountされている要素をunmountする
  cleanup();
});
```

`import "@testing-library/jest-dom";`については後続の記事で詳述しようと思っていますが、[`@testing-library/jest-dom`](https://www.npmjs.com/package/@testing-library/jest-dom) が提供しているcustom matchers を有効化するために記載しています。
`toBeDisabled`や`toBeInTheDocument`など、テストをより適切に実行できるようにするための便利なassertionが用意されています。

`vitest.config.ts`のコメント部分にも記載していますが、オートインポートが無くセットアップファイルでの`import`のみでもテストに用いることができますが、`VSCode`上でのエラーが発生してしまうのでオートインポートをしていました。
```TypeScript
// setupTests.tsで実際のimportが実行される。
// ここが無くてもtestは通るが、型推論が働かないためauto-importを有効化
{ "@testing-library/jest-dom": ["matchers"] }
```

以上で、テスト実行用の環境構築は完了です!
簡単なテストを書いてみて、テストが実行されることを確認しましょう。

``` sample.test.ts
// src/__tests__/sample.test.ts
describe("サンプル", () => {
  test("1+1=2であること", () => {
    expect(1 + 1).toBe(2);
  }
})
```



# 終わりに
いかがだったでしょうか。
今後の記事では「DOMの挙動に関するテスト」「APIなど通信を伴う処理のテスト」に関して記事を作成しようと思っております。

ご指摘などありましたら、ぜひコメントをいただけますと幸いです m(_ _)m