← トップページに戻る

TDD実践ガイド

📝第2章 TODOアイテムの表示(静的データ、App Router、TypeScript)

TODOアプリの最初の機能として、単一のTODOアイテムを表示するClient ComponentをTDDで開発します。 App Routerでは、インタラクティブなUI要素はClient Componentとして作成することが一般的です。

TODOリスト

  • TodoItemコンポーネントは、TODOのテキストとIDを表示する
  • TodoItemコンポーネントは、完了状態に応じてスタイル(打ち消し線など)を切り替える

🤔テストって何を検証できるの?

「a + b = c」だけじゃない!テストの世界

多くの人がテストと聞くと「add(2, 3) === 5」のような 数値の計算結果を確認するものを想像するかもしれません。でも実際のWebアプリケーションでは、 もっと複雑で実用的なことを自動で検証できるんです!

従来のイメージ

  • • 数値の計算結果
  • • 文字列の変換
  • • 配列の操作
例:シンプルな関数テスト
expect(add(2, 3)).toBe(5);
expect(capitalize('hello')).toBe('Hello');

🚀実際にできること

  • • 画面に正しいテキストが表示される
  • • ボタンを押したら状態が変わる
  • • 条件によってスタイルが切り替わる
  • • フォームの入力値が正しく処理される
  • • エラー時に適切なメッセージが出る

🎭この章で体験すること:「状態」と「見た目」のテスト

今回作るTodoItemコンポーネントでは、 以下のような「実際のユーザーが体験すること」を自動でテストします:

📝 テキスト表示のテスト

「App RouterでTDDを学ぶ」というTODOを渡したら、画面にそのテキストが表示されるか?

expect(screen.getByText(/App RouterでTDDを学ぶ/)).toBeInTheDocument();

🎨 スタイル変化のテスト

TODOが完了状態(completed: true)の時、 テキストに打ち消し線が付くか?

expect(todoTextElement).toHaveStyle('text-decoration: line-through');

🔄 状態による条件分岐のテスト

未完了状態(completed: false)の時は、 打ち消し線が付かないか?

expect(todoTextElement).not.toHaveStyle('text-decoration: line-through');

💡なぜこれが革命的なのか?

🤖自動化:手動でブラウザを開いて確認する必要がない

高速:数秒で何十ものパターンを検証できる

🛡️安全:コードを変更しても既存機能が壊れていないことを即座に確認

📋仕様書:テストコード自体が「このコンポーネントはこう動くべき」という仕様書になる

🔴1. レッド:失敗するテストを書く (TodoItemコンポーネントのテキスト表示)

まず、TodoItemコンポーネントのテストファイルを作成します。components フォルダーを作成し、 その中に TodoItem.test.tsx を作成します。

components/TodoItem.test.tsx
import { render, screen } from '@testing-library/react';
import TodoItem, { Todo } from './TodoItem'; // TodoItem.tsx と型定義はまだ存在しない

describe('TodoItemコンポーネントのテスト', () => {
  it('TODOのテキストとIDを表示すること', () => {
    const todo: Todo = { id: 1, text: 'App RouterでTDDを学ぶ', completed: false };
    render(<TodoItem todo={todo} />);
    // 'App RouterでTDDを学ぶ' というテキストがドキュメント内に存在することを確認
    expect(screen.getByText(/App RouterでTDDを学ぶ/i)).toBeInTheDocument();
    // IDも表示されていることを確認 (例: #1)
    expect(screen.getByText(/#1/i)).toBeInTheDocument();
  });
});

テストを実行します。components/TodoItem.tsx が存在しないため、テストは失敗します(レッド)。

npm test

🟢2. グリーン:テストをパスさせる最小限のコードを書く

components/TodoItem.tsx を作成し、 テストをパスさせる最小限のコードを書きます。Client Componentであることを明示するために、 ファイルの先頭に "use client"; を記述します。

components/TodoItem.tsx
"use client"; // Client Componentであることを示す

import React from 'react';

export interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

interface TodoItemProps {
  todo: Todo;
}

export default function TodoItem({ todo }: TodoItemProps) {
  return (
    <li>
      <span>#{todo.id}</span>: <span>{todo.text}</span>
    </li>
  );
}

再度テストを実行します。 すべてのテストが成功するはずです(グリーン)。

npm test

🔵3. リファクタリング:コードを改善する

現時点ではとくにリファクタリングする箇所はなさそうです。 もしコンポーネントの構造やPropsの扱いで改善点があれば、このタイミングで行います。 たとえば、IDの表示形式をもっと明確にするために spanを使うなどが考えられますが、今回はこのまま進めます。

📋TODOリストの次の項目へ:完了状態のスタイル

🔴1. レッド:失敗するテストを書く (完了状態のスタイル)

components/TodoItem.test.tsx に、 完了状態に応じてスタイルが変わることを確認するテストを追加します。 ここでは、完了時に text-decoration: line-throughスタイルが適用されることを想定します。

components/TodoItem.test.tsx (追加分)
  it('TODOが完了状態の時に打ち消し線スタイルが適用されること', () => {
    const todo: Todo = { id: 2, text: 'スタイルのテストを書く', completed: true };
    render(<TodoItem todo={todo} />);
    // テキスト部分の要素を取得してスタイルを確認
    const todoTextElement = screen.getByText(/スタイルのテストを書く/i);
    expect(todoTextElement).toHaveStyle('text-decoration: line-through');
  });

  it('TODOが未完了状態の時に打ち消し線スタイルが適用されないこと', () => {
    const todo: Todo = { id: 3, text: 'コンポーネントコードをリファクタリングする', completed: false };
    render(<TodoItem todo={todo} />);
    const todoTextElement = screen.getByText(/コンポーネントコードをリファクタリングする/i);
    expect(todoTextElement).not.toHaveStyle('text-decoration: line-through');
  });

テストを実行します。 新しい2つのテストが失敗するはずです(レッド)。

npm test

🟢2. グリーン:テストをパスさせる最小限のコードを書く

components/TodoItem.tsx (修正版)
"use client";

import React from 'react';

export interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

interface TodoItemProps {
  todo: Todo;
}

export default function TodoItem({ todo }: TodoItemProps) {
  const textStyle = {
    textDecoration: todo.completed ? 'line-through' : 'none',
  };

  return (
    <li>
      <span>#{todo.id}</span>: <span style={textStyle}>{todo.text}</span>
    </li>
  );
}

再度テストを実行します。 すべてのテストが成功するはずです(グリーン)。

npm test

⚛️app/page.tsx でコンポーネントを使用

開発した TodoItem コンポーネントを ホームページ (app/page.tsx) で表示してみましょう。page.tsx はデフォルトでServer Componentとして動作します。 Client Componentである TodoItem を使用するために、TodoItem 側で"use client"; を記述しています。

app/page.tsx
import TodoItem, { Todo } from '@/components/TodoItem'; // '@/' は app/ を指すエイリアス

export default function HomePage() {
  // サンプルデータ (Server Componentなのでここでデータ取得ロジックが入ることも)
  const sampleTodos: Todo[] = [
    { id: 1, text: 'Next.js 15 App Routerを学ぶ', completed: false },
    { id: 2, text: 'TypeScriptでTDDアプリを構築する', completed: true },
    { id: 3, text: 'GitHub Actions CIをセットアップする', completed: true },
  ];

  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
      <div className="z-10 w-full max-w-5xl items-center justify-between font-mono text-sm lg:flex flex-col">
        <h1 className="text-4xl font-bold mb-8">My TODO List</h1>
        {sampleTodos.length > 0 ? (
          <ul className="w-full">
            {sampleTodos.map((todo) => (
              <TodoItem key={todo.id} todo={todo} />
            ))}
          </ul>
        ) : (
          <p>No todos yet!</p>
        )}
      </div>
    </main>
  );
}

開発サーバーを起動し、http://localhost:3000 をブラウザーで確認すると、 TODOアイテムが表示され、完了済みのものには打ち消し線が付いているはずです。

npm run dev

🚀コミットとCIの確認

git add .
git commit -m "feat: Implement TodoItem component with TDD (Next.js 15, TypeScript)"
git push

GitHub ActionsでCIがパスすることを確認してください。

🎯まとめ

これで第2章は完了です。TodoItemコンポーネントをTDDで開発し、App Routerのページで表示することができました。 次の章では、TODOアイテムを複数まとめて管理する TodoListコンポーネントや、新しいTODOを追加するためのフォームを開発します。