Vite で作った React プロジェクトに最小構成の Jest を導入する

#  はじめに

プログラミング言語をとわず設定ファイルは難しいですよね :thinking: ドキュメントを見ても何ができるのかわからず検索でたまたま見つけた設定ファイルをコピペしては試し...を繰り返していると思います。わたしは Webpack で痛い思いをしたので本記事では設定ファイルを少なく、ドキュメントを引用して再現性を高くすることを意識して Vite で作った React プロジェクトに最小構成の Jest を導入します。

# 環境構築

## プロジェクト作成

TypeScript + React で作成してください。

```
npm create vite@latest
```

## Jest

テストフレームワークは Jest を選択したのでインストールします。

```
npm install --save-dev jest
```

必要なパッケージは次の `ts-jest` から順にスクロールしながら進めると OK です。

## `ts-jest`

Jest を TypeScript で動かすためのプリプロセッサです。このプリプロセッサでは TypeScript で書いた Jest テストを JavaScript に変換する役割があります。

```
npm install --save-dev ts-jest
```

インストールするだけでは有効にならず設定ファイル（ `jest.config.ts` ）が必要になります。コマンド実行時には `jest.config.js` が作成されるので TypeScript ファイルに変換しましょう（変換する必要はありませんが、統一したいので変換しています）

```
npx ts-jest config:init
```

## jest.config.ts で考えること

- .js を .ts に変換する
- preset は `"ts-jest"` とする
  - transformer は preset の指定をすると内部で設定されるので省略
- testEnvironment は DOM 環境をブラウザで動いてるかのようにみせるため `jsdom` に変換

```jest.config.ts
import type {Config} from 'jest';

const config: Config = {
  preset: "ts-jest",
  testEnvironment: "jsdom",
};

export default config;
```

:::note

`jest.config.js` は次のような内容になっています。

```jest.config.js
/** @type {import('ts-jest').JestConfigWithTsJest} **/
export default {
  testEnvironment: "node",
  transform: {
    "^.+.tsx?$": ["ts-jest",{}],
  },
};
```
:::

:::note
`npx jest --init` も良いのですが TypeScript の項目を上書きすることになるので `npx ts-jest config:init` が楽です

URL: https://kulshekhar.github.io/ts-jest/docs/getting-started/installation/#jest-config-file:~:text=You%20can%20also,js%20file%20afterwards.
:::

## `jest-environment-jsdom`

`testEnvironment` に `jsdom` が指定してあります。`jsdom` はコンポーネントがブラウザで動いてるかのように DOM 環境を提供するパッケージです。

```
npm install --save-dev jest-environment-jsdom
```

## `@types/jest`

TypeScript を選択すると型定義した Jest API（ `descripbe` や `expect` ）が必要になります。サードパーティのパッケージ `@types/jest` が必要になります。

```
npm install --save-dev @types/jest
```

:::note
`@jest/globals` も良いのですが必要な関数を import で取り込む必要があるので @types/jest が推奨されています

URL: https://jestjs.io/docs/getting-started#:~:text=Or%20you%20may%20choose%20to%20install%20the%20%40types/jest%20package.%20It%20provides%20types%20for%20Jest%20globals%20without%20a%20need%20to%20import%20them.
:::

## `ts-node`

Jest の実行に `ts-node` が必要です。

```
npm install --save-dev ts-node
```

:::note alert
もしインストールせず Jest を実行すると次のエラーになります。

```
Error: Jest: Failed to parse the TypeScript config file /Users/akira/Project/private/vite-ts/jest.config.ts
  Error: Jest: 'ts-node' is required for the TypeScript configuration files. Make sure it is installed
```
:::


## React Testing Library

Jest で React コンポーネントのテストを作成するのに必要です。

```
npm install --save-dev @testing-library/react @testing-library/dom @types/react @types/react-dom
```

## `jest-dom` インストール

React コンポーネントのテストに必要なカスタムマッチャーを追加します。

```
npm install --save-dev @testing-library/jest-dom
```

`jest-dom` を有効にするにはテストファイルの中で import する必要がありますが都度、テストファイルに追加する必要がありとても手間です。Jest には `setupFilesAfterEnv` を使ってセットアップのスクリプトを指定することが出来るのでそれを利用します。また、公式ドキュメントによると tsconfig.json で `include` に指定する必要があります。

URL: https://github.com/testing-library/jest-dom?tab=readme-ov-file#with-typescript

### jest.config.ts で考えること

- vite で作った tsconfig.json は project references を使っている
- Jest では project references で参照するファイルの内容までみない
- Jest で必要な tsconfig を新しく作成して transform で指定する
  - tsconfig.jest.json として tsconfig.app.json からコピペ
- setupFilesAfterEnv にセットアップスクリプトを指定する
  - jest.setup.ts として jest-dom を import で呼び出す

```diff
diff --git a/jest.config.ts b/jest.config.ts
index 95b57b9..c99ca4d 100644
--- a/jest.config.ts
+++ b/jest.config.ts
@@ -2,6 +2,12 @@ import type {Config} from 'jest';
 
 const config: Config = {
   preset: "ts-jest",
+  transform: {
+    "^.+.tsx?$": ["ts-jest", {
+      tsconfig: "tsconfig.jest.json"
+    }],
+  },
+  setupFilesAfterEnv: ['./jest.setup.ts'],
   testEnvironment: "jsdom",
 };
```

:::note warn
公式ドキュメントにある構造だと WARN になります。
```
// jest.config.js
module.exports = {
  // [...]
  globals: {
    'ts-jest': {
      tsConfig: 'tsconfig.test.json'
    }
  }
};
```
URL: https://huafu.github.io/ts-jest/user/config/tsConfig#path-to-a-tsconfig-file

WARN メッセージ

```
ts-jest[ts-jest-transformer] (WARN) Define `ts-jest` config under `globals` is deprecated. Please do
transform: {
    <transform_regex>: ['ts-jest', { /* ts-jest config goes here in Jest */ }],
},
```
:::

### jest.setup.ts で考えること

ここは jest-dom を import を使って呼び出すだけで OK です。

```jest.setup.ts
import "@testing-library/jest-dom"
```

### tsconfig.jest.json で考えること

- tsconfig.app.json をコピペして include にセットアップスクリプトを指定する
- テストだけなら不要な設定が多いかも（要ブラッシュアップ）

```tsconfig.jest.json
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "isolatedModules": true,
    "moduleDetection": "force",
    "noEmit": true,
    "jsx": "react-jsx",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src", "jest.setup.ts"]
}
```

パッケージの追加、設定はここまでです。

## アセット

Jest は Webpack などバンドラといっしょに動くわけではないので svg / css を理解することができません。そのため、`moduleNameMapper` を使ってマッチした拡張子に対してスクリプトを実行することができます。

svg / css があったとしてもコンポーネントの振る舞いに影響がないので `module.exports = {};` を実行するだけで OK です。

```tests/__mocks__/fileMock.ts
module.exports = {};
```

```tests/__mocks__/styleMock.ts
module.exports = {};
```

```diff
diff --git a/jest.config.ts b/jest.config.ts
index c99ca4d..624a38a 100644
--- a/jest.config.ts
+++ b/jest.config.ts
@@ -7,6 +7,10 @@ const config: Config = {
       tsconfig: "tsconfig.jest.json"
     }],
   },
+  moduleNameMapper: {
+    "\\.svg": "<rootDir>/tests/__mocks__/fileMock.ts",
+    "\\.css": "<rootDir>/tests/__mocks__/styleMock.ts",
+  },
   setupFilesAfterEnv: ['./jest.setup.ts'],
   testEnvironment: "jsdom",
 };
```

# テストを動かしてみる

```
import { render, screen } from "@testing-library/react"
import App from "../src/App.tsx"

test("render App component", async () => {
  render(<App />)

  const h1 = screen.getByRole("heading")
  expect(h1).toHaveTextContent("Vite + React")
})
```

これを `npm run test` から実行してみてください。きっとパスするはずです。

# 気づき

TypeScript で動かそうと思うと Jest 以外のパッケージに依存して、かつ初期化の方法が複数あったりとかなりドキュメントを読み込まないと理解することができませんでした。

# リポジトリ

https://github.com/guppy0356/vite-ts

# JISOUのメンバー募集中！
プログラミングコーチングJISOUでは、新たなメンバーを募集しています。
日本一のアウトプットコミュニティでキャリアアップしませんか？
興味のある方は、ぜひホームページをのぞいてみてください！
▼▼▼

https://projisou.jp