Skip to content

単体テスト作成

cf.

事前準備

  • Vitestをインストールします:
npm install -D vitest
  • package.jsontestスクリプトを追加します:
{
  "scripts": {
    "test": "vitest"
  }
}
npm install --save-dev @testing-library/react @testing-library/user-event
  • Vites向けのパッケージをインストールします:
npm install --save-dev \
  @testing-library/svelte \
  @testing-library/jest-dom \
  @sveltejs/vite-plugin-svelte \
  vitest \
  jsdom
  • vitest-setup.jsを下記内容で作成します:
import "@testing-library/jest-dom/vitest";
  • vite.config.tsを下記内容で更新します:
import { svelte } from "@sveltejs/vite-plugin-svelte";
import { svelteTesting } from "@testing-library/svelte/vite";
import { defineConfig } from "vitest/config";

export default defineConfig({
  plugins: [svelte(), svelteTesting()],
  test: {
    environment: "jsdom",
    setupFiles: ["./vitest-setup.js"],
  },
});
  • src/test-util.tsxを作成します:
import { render, type RenderResult } from "@testing-library/react";
import { Provider } from "react-redux";

import { store } from "./app/store";

/**
 * テスト対象のUIについてRedux storeを付与してレンダリングします。
 *
 * @param ui テスト対象コンポーネント
 * @returns Redux store付きレンダリング
 */
export const renderWithProvider = (ui: React.ReactElement): RenderResult => {
  return render(<Provider store={store}>{ui}</Provider>);
};

テスト作成

Counter.test.tsx

Increment, Decrementボタンを持つコンポーネントについて、ボタン操作を行なってカウントが更新されることをテストします:

import "@testing-library/jest-dom/vitest";

import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";

import { renderWithProvider } from "../../test-utils";
import { Counter } from "./Counter";

describe("Counter component", () => {
  it("Increments and decrements the count", async () => {
    renderWithProvider(<Counter title="Unit test" />);

    expect(screen.getByText("Unit test")).toBeInTheDocument();

    const incrementButton = screen.getByText("Increment");
    const decrementButton = screen.getByText("Decrement");
    const countDisplay = screen.getByText("0");

    await userEvent.click(incrementButton);
    expect(countDisplay).toHaveTextContent("1");

    await userEvent.click(incrementButton);
    expect(countDisplay).toHaveTextContent("2");

    await userEvent.click(decrementButton);
    expect(countDisplay).toHaveTextContent("1");
  });
});

Auth.test.tsx

ユーザ名およびパスワードを入力してログインAPIを呼び出すコンポーネントについて、ログイン成否によって画面上のメッセージが適切に表示されるかテストします。authApiクラスに実装したloginApi関数をモック化しています:

import { cleanup, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import { renderWithProvider } from "../../test-utils";
import { Auth } from "./Auth";
import * as authApi from "./authApi";

describe("Auth component", () => {
  let u: ReturnType<typeof userEvent.setup>;

  beforeEach(() => {
    u = userEvent.setup();
  });

  afterEach(() => {
    vi.restoreAllMocks();
    cleanup();
  });

  it("Login success", async () => {
    vi.spyOn(authApi, "login").mockImplementation(async (req) => {
      if (req.name !== "nob" || req.password !== "passwd") {
        throw new Error("テスト失敗"); // 入力値が関数に正しく渡っていなければテスト失敗
      }
      return Promise.resolve({ ok: true, valid: true });
    });

    renderWithProvider(<Auth />);

    const nameInput = screen.getByLabelText("name");
    const passwordInput = screen.getByLabelText("password");

    await u.type(nameInput, "nob");
    await u.type(passwordInput, "passwd");
    await u.click(screen.getByText("ログイン"));

    expect(screen.getByText("ログイン完了")).toBeInTheDocument();
  });

  it("Login failed", async () => {
    vi.spyOn(authApi, "login").mockImplementation(async (req) => {
      if (req.name !== "nob" || req.password !== "passwd") {
        throw new Error("テスト失敗"); // 入力値が関数に正しく渡っていなければテスト失敗
      }
      return Promise.resolve({ ok: false, message: "単体テスト上のエラー" });
    });

    renderWithProvider(<Auth />);

    const nameInput = screen.getByLabelText("name");
    const passwordInput = screen.getByLabelText("password");

    await u.type(nameInput, "nob");
    await u.type(passwordInput, "passwd");
    await u.click(screen.getByText("ログイン"));

    expect(screen.getByText("単体テスト上のエラー")).toBeInTheDocument();
  });

  it("Fail to call API", async () => {
    vi.spyOn(authApi, "login").mockImplementation(async () => {
      throw new Error("テスト失敗"); // APIとの疎通失敗想定
    });

    renderWithProvider(<Auth />);

    const nameInput = screen.getByLabelText("name");
    const passwordInput = screen.getByLabelText("password");

    await u.type(nameInput, "nob");
    await u.type(passwordInput, "passwd");
    await u.click(screen.getByText("ログイン"));

    expect(
      screen.getByText("不明なエラーが発生しました。"),
    ).toBeInTheDocument();
  });
});

テスト起動

  • 下記コマンドでプロジェクト内のテストを一括実行します:
npm run test
  • ディレクトリを指定することで特定のコンポーネントのみテストできます:
npm run test src/features/{component}

カバレッジ出力

cf. https://vitest.dev/guide/coverage.html

  • パッケージをインストールします:
npm i -D @vitest/coverage-v8
  • vite.config.tstest.coverageを追加します:
  test: {
    coverage: {
      provider: "v8",
      include: ["src/features"], // features配下のみカバレッジ集計
      exclude: ["src/features/**/*Api*.ts", "src/features/**/*module.scss"] // xxxApi.tsはモック化するのでカバレッジ対象外、scssも対象外
    }
  }
  • package.jsonにカバレッジ出力スクリプトを追加します:
{
  "scripts": {
    "coverage": "vitest run --coverage"
  }
}
  • 下記コマンドを実行するとcoverageディレクトリ配下にカバレッジレポートが出力されます:
npm run coverage