Open4

フロントエンドテスト

いるかいるか

Headerコンポーネント

Header.tsx
import Link from "next/link";

export type HeaderProps = {
	title?: string;
};

export const Header = ({ title = "YNU-econ 単位チェッカー" }: HeaderProps) => {
	return (
		<header className="flex items-center justify-center bg-brand p-4 shadow-sm">
			<Link
				href="/"
				className="text-white hover:text-gray-200 transition-colors"
			>
				<h1 className="font-semibold text-xl">{title}</h1>
			</Link>
		</header>
	);
};

テスト
書いてる途中、それっぽいテストも書けたし大満足!カバレッジもよし

Header.test.tsx
import { render } from "@testing-library/react";
import { describe, expect, test } from "vitest";
import { Header } from "../Header";

describe("Header コンポーネント", () => {
	test("デフォルトのタイトルで表示される", () => {
		const { container } = render(<Header />);
		expect(container).toMatchSnapshot();
	});

	test("カスタムタイトルで表示される", () => {
		const { container } = render(<Header title="単位チェックアプリ" />);
		expect(container).toMatchSnapshot();
	});
});

本当にその気持ちあってる?何のためのテストかわかってる?テストすること自体が目的になっていない?

  • スナップショットテストの意味があるのか?
  • 本当にテストしたいのはHeaderを押したときにしっかりとhomeに移動するふるまいではないのか?
  • props に定義されたものは必ずコンポーネントの中で何らかの処理を行うものであるため。テストケースに含めることでコンポーネントの振る舞いをテストするべきではないのか?
いるかいるか

これでいい感じ

describe("Header コンポーネント", () => {
	test("カスタムタイトルを表示する", () => {
		const title = "単位チェックアプリ";
		render(<Header title={title} />);
		expect(
			screen.getByRole("heading", { level: 1, name: title }),
		).toBeInTheDocument();
	});

	test("デフォルトタイトルを表示する", () => {
		render(<Header />);
		expect(
			screen.getByRole("heading", {
				level: 1,
				name: "YNU-econ 単位チェッカー",
			}),
		).toBeInTheDocument();
	});

	test("タイトルのリンク先は / である", () => {
		render(<Header />);
		const link = screen.getByRole("link");
		expect(link).toHaveAttribute("href", "/");
	});
});

いるかいるか

1. 課題認識

「スナップショットテストは UI をちょっと直しただけで失敗するので、ノイズが多くなりがちです。その結果『どうせ更新すれば通る』と軽視される危険があると感じています。」

2. 自分の考え・方針

「そのため私は “何を保証したいか” を明確にして、必要な場合にだけスナップショットを使うべき だと考えています。
具体的には DOM 構造が分岐するようなコンポーネントや、意図せぬ変更が混ざりやすい部分でだけ活用します。」

3. 実際の工夫

「一方で『タイトル文言が変わる』のようなケースでは、スナップショットではなく toHaveTextContenttoHaveAttribute のように “ユーザーが観測する挙動” にフォーカスしたテストを書きます。
つまり UI 構造の差分検知はスナップショット、挙動の保証は明示的なアサーション、と使い分けています。」

4. 戦略へのつなぎ

「このように テスト戦略の中でスナップショットの位置づけを下層の補助的なものと定義し、基本はユニット・統合で “振る舞いを保証するテスト” を重視するようにしています。」

「最初はスナップショットを使っていましたが、UI が固定的だったので差分検知にあまり意味がなく、実際にはタイトル文言やリンク先を直接アサートする形にしました。こうした見直しを通じて、テストは “何を保証したいか” を明確にして書くことが大事だと考えるようになりました。」

いるかいるか

Vitestが @/lib/utils のパスエイリアスを解決できないエラー

import react from "@vitejs/plugin-react";
import { coverageConfigDefaults, defineConfig } from "vitest/config";
import tsconfigPaths from "vite-tsconfig-paths"; // これを追加

export default defineConfig({
	plugins: [react(), tsconfigPaths()],
	test: {
		environment: "jsdom",
		setupFiles: "./vitest.setup.ts",
		globals: true,
		coverage: {
			provider: "v8",
			reporter: ["text", "json", "html"],
			include: ["src/**/*.{ts,tsx,js,jsx}"],
			exclude: [
				...coverageConfigDefaults.exclude,
				"**/*.stories.{ts,tsx,js,jsx}",
				"**/*.test.{ts,tsx,js,jsx}",
				"**/index.{ts,tsx,js,jsx}",
				"src/components/ui/**", // shadcn/ui コンポーネントを除外
			],
		},
	},
});

必要なパッケージの確認:

bun add -D vite-tsconfig-paths
  • tsconfig.jsonのパスエイリアスが自動で反映される