Open4

非Vite環境にVitestによる単体テストを導入する

elpntelpnt

やりたいこと

  • Next.js プロジェクトの単体テストを jest から vitest に乗り換える
  • Reactコンポーネントのテスト

セットアップ

npx create-next-app --ts

必要なパッケージのインストール

npm i -D vitest @vitejs/plugin-react \ 
  @testing-library/react @testing-library/jest-dom jsdom
elpntelpnt

各種設定

プロジェクトのルートディレクトリに vitest.config.ts を作成し、以下を記入する

vitest.config.ts
/// <reference types="vitest" />
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true, // use `describe, it, test` without importing them
    environment: "jsdom",
    setupFiles: "./tests/setup.ts",
  },
});

vitest の globals import はこのままだと TypeScript が構文エラーを起こすので tsconfig.json も編集する

tsconfig.json
{
  "compilerOptions": {
    // ...
    "types": ["vitest/globals"]
  }
}

@testing-library/jest-dom.toBeInTheDocument() などの matcher が全テストファイルで使えるよう、vitest.config.ts で指名したセットアップファイル <rootDir>/tests/setup.ts にインポートする

tests/setup.ts
import "@testing-library/jest-dom";
elpntelpnt

実践例

HeadlessUI の Listbox で作ったセレクターコンポーネントをテストする

npm i @headlessui/react
components/SelectMenu.tsx
import { useState } from "react";
import { Listbox } from "@headlessui/react";

const options = ["Apple", "Banana", "Orange"];

export const SelectMenu = () => {
  const [selectedFruit, setSelectedFruit] = useState(options[0]);
  return (
    <Listbox value={selectedFruit} onChange={setSelectedFruit}>
      <Listbox.Button>{selectedFruit}</Listbox.Button>
      <Listbox.Options>
        {options.map((option) => (
          <Listbox.Option key={option} value={option}>
            {option}
          </Listbox.Option>
        ))}
      </Listbox.Options>
    </Listbox>
  );
};
components/SelectMenu.test.tsx
import { SelectMenu } from "./SelectMenu";
import { render, screen, fireEvent } from "@testing-library/react";

describe("SelectMenu test", () => {
  it("should render and select", () => {
    render(<SelectMenu />);

    expect(screen.queryByText("Apple")).toBeInTheDocument();
    expect(screen.queryByText("Orange")).not.toBeInTheDocument();

    // Open the listbox and select 'Orange'
    fireEvent.click(screen.getByText("Apple"));
    fireEvent.click(screen.getByText("Orange"));

    expect(screen.queryByText("Apple")).not.toBeInTheDocument();
    expect(screen.queryByText("Orange")).toBeInTheDocument();
  });
});
elpntelpnt

テスト実行

package.json
{
  "scripts": {
    // ...
    "test": "vitest run"
  }
}
npm run test

> test
> vitest run


 RUN  /home/elpnt/dev/mock/nextjs-vitest

 √ components/SelectMenu.test.tsx (1)

Test Files  1 passed (1)
     Tests  1 passed (1)
      Time  1.93s (in thread 51ms, 3792.20%)