投稿目的
・備忘録
・よりよい方法をご教示いただきたい
はじめに
先日、TS×React×Express×Firebase×Render でアプリケーションを作りました という記事を投稿させていただきました。
フロントエンドを構築している際、「せっかくだから単体テスト導入したいな~」と思っていましたがうまくいかず...
結局「フロントエンドをローカル起動」 → 「バックエンドをローカル起動 + Firebaseに直接リクエスト」という方法で、アプリケーションの挙動を確かめていました。大変非効率です...
「入力値が6文字未満の場合はボタンが非活性になっているか?」「入力漏れのバリデーションが発動した際、Alert
上に表示される文言は想定通りになっているか?」などを確かめる場合、アプリケーションを起動してのデバッグよりも単体テスト実施の方がよりスピーディーですし、想定外の事象の見落としを防ぎやすくなります。
※期待値と異なる場合、↓のようにそのことを教えてくれます。
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
環境であることを前提としています。
// テスト関連のパッケージ類
$ 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
package.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",
...
},
テスト用のコマンドの用意
package.json
にtest
コマンドを追加しましょう。
※bun
を使った場合、bun test
を実行するとvitest
ではなくbun
自体によるテストが実行されてしまいます... (bun run test
とすれば問題ありませんが)
↑については、test
→vitest
などとすることで、bun vitest
でテストを実行できます。
"scripts": {
"test": "vitest --config vitest.config.ts",
...
},
vitest.config.ts
の用意
create vite
でアプリケーションを作成すると、vite.config.ts
が自動生成されると思います。
似たような形式で、vitest.config.ts
を作成していきます。
vite.config.ts
※@tanstack/react-router
環境でのvite.config.ts
を持ってきました。内容はここでは関係ありません。
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"]
}
}
}
}
});
vitest
公式にもある通り、既存のvite.config.ts
に追記することもできますし、今回のようにvitest.config.ts
を作成するなど、別途Configファイルを作成することもできます。
まずはvitest.config.ts
の内容を以下に示します。
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>)
→ 2つのConfigをマージします。今回は、もともとvite.config.ts
に定義されているConfigと、本ファイルで定義するConfigをマージするために使います。・
test.globals
→いちいちimport
しなくてもvi.mock()
やrender()
を使いたいのでtrue
に設定します。・
test.environment
→componentの状態にように、ブラウザ上でのアプリケーションの動作をテストしたい場合はjsdom
かhappy-dom
を選択する必要があるようです。・
test.setupFiles
→各テストファイルを実行する前に読み込まれるファイルのパスを設定します・
unplugin-auto-import
→指定したモジュールについて、各ファイルでいちいちimport
をしなくても活用できるようにしたいため (オートインポート)、このパッケージを用います。
→Config内のplugins
内で、デフォルトインポートしたAutoImport
を実行し、引数のConfig内ににオートインポートしたい内容を記述していきます。
unplugin-auto-import
のConfigについて、追記します。
・モジュールすべてをオートインポートしたい場合は、下記のようにモジュール名をimports
の配列に配置します。
plugins: [
AutoImport({
imports: [ "vitest" ]
})
]
・モジュール内の特定のモジュール(名前付きexport
されているもの)をオートインポートしたい場合は、{ <パッケージ名> : <オートインポートさせたいモジュール名の配列> } をimports
の配列に配置します。
途中の["screen", "testScreen"]
については、screen
というモジュールをtestScreen
というエイリアスでオートインポートできるようになります。
※@testing-library/react
のscreen
をそのままオートインポートすると、グローバル変数のscreen
と解釈されてしまいエラーが発生したため、エイリアスを設定しています。
plugins: [
AutoImport({
imports: [ {
"@testing-library/react": [
"render",
"waitFor",
"act",
"renderHook",
"cleanup",
["screen", "testScreen"]
]
}]
})
]
・デフォルトexport
されているモジュールをオートインポートしたい場合は、{ <パッケージ名> : [<オートインポートする際のモジュール名>] } をimports
の配列に配置します。
plugins: [
AutoImport({
imports: [ { "@testing-library/user-event": ["userEvent"] }]
})
]
上記のようにオートインポートの設定を記述し、先ほど定義したテストコマンドを実行 (npm run test
等) すると、dts
で定義したパスのファイルにオートインポートを有効化するためのファイルが自動生成されます。
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']
}
セットアップ用のファイルを作成
先ほどのvitest.config.ts
作成時、test.setupFiles
を定義しました。
環境構築の最後に、上記で定義したセットアップ用ファイルを整備しましょう。
私の場合は、以下のようになりました。
先ほどのオートインポートが効いているので、afterEach
・vi
・cleanup
はimportしなくても良くなっています!
import "@testing-library/jest-dom";
afterEach(() => {
// mockをリセットする
vi.resetAllMocks();
vi.clearAllMocks();
// mountされている要素をunmountする
cleanup();
});
import "@testing-library/jest-dom";
については後続の記事で詳述しようと思っていますが、@testing-library/jest-dom
が提供しているcustom matchers を有効化するために記載しています。
toBeDisabled
やtoBeInTheDocument
など、テストをより適切に実行できるようにするための便利なassertionが用意されています。
vitest.config.ts
のコメント部分にも記載していますが、オートインポートが無くセットアップファイルでのimport
のみでもテストに用いることができますが、VSCode
上でのエラーが発生してしまうのでオートインポートをしていました。
// setupTests.tsで実際のimportが実行される。
// ここが無くてもtestは通るが、型推論が働かないためauto-importを有効化
{ "@testing-library/jest-dom": ["matchers"] }
以上で、テスト実行用の環境構築は完了です!
簡単なテストを書いてみて、テストが実行されることを確認しましょう。
// src/__tests__/sample.test.ts
describe("サンプル", () => {
test("1+1=2であること", () => {
expect(1 + 1).toBe(2);
}
})
終わりに
いかがだったでしょうか。
今後の記事では「DOMの挙動に関するテスト」「APIなど通信を伴う処理のテスト」に関して記事を作成しようと思っております。
ご指摘などありましたら、ぜひコメントをいただけますと幸いです m(_ _)m