1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

投稿目的

・備忘録
・よりよい方法をご教示いただきたい

はじめに

先日、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.jsontestコマンドを追加しましょう。

bunを使った場合、bun testを実行するとvitestではなくbun自体によるテストが実行されてしまいます... (bun run testとすれば問題ありませんが)
↑については、testvitestなどとすることで、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の状態にように、ブラウザ上でのアプリケーションの動作をテストしたい場合はjsdomhappy-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/reactscreenをそのままオートインポートすると、グローバル変数の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
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を定義しました。
環境構築の最後に、上記で定義したセットアップ用ファイルを整備しましょう。

私の場合は、以下のようになりました。
先ほどのオートインポートが効いているので、afterEachvicleanupは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 が提供しているcustom matchers を有効化するために記載しています。
toBeDisabledtoBeInTheDocumentなど、テストをより適切に実行できるようにするための便利なassertionが用意されています。

vitest.config.tsのコメント部分にも記載していますが、オートインポートが無くセットアップファイルでのimportのみでもテストに用いることができますが、VSCode上でのエラーが発生してしまうのでオートインポートをしていました。

// 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

1
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?