← トップページに戻る

TDD実践ガイド

🧪第14章 学習用テストと回帰テスト(TODO完了切り替え)

Kent Beckの「学習用テスト」の概念を実践し、TODOアイテムの完了状態を切り替える機能を実装します。 また、回帰テストの重要性についても学びます。

学習用テストは、サードパーティのソフトウェアの動作を理解するために書くテストです。 これらのテストは、外部ライブラリやAPIの動作を確認し、期待通りに動作することを保証します。
— Kent Beck『テスト駆動開発』第14章より

📋TODOリスト

  • TODOアイテムの追加機能
  • TODOアイテムの完了切り替え機能
  • • 学習用テストの実践
  • • 回帰テストの実行

📚1. 学習用テスト:React Testing Libraryの動作確認

まず、React Testing Libraryのチェックボックス操作について学習用テストを書いて動作を確認します。

__tests__/learning-tests/checkbox-behavior.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';

// 学習用テスト:チェックボックスの動作確認
describe('学習用テスト: チェックボックスの動作', () => {
  it('チェックボックスの状態変更を正しく検出できること', () => {
    const TestComponent = () => {
      const [checked, setChecked] = React.useState(false);
      return (
        <label>
          <input
            type="checkbox"
            checked={checked}
            onChange={(e) => setChecked(e.target.checked)}
          />
          テストチェックボックス
        </label>
      );
    };

    render(<TestComponent />);
    
    const checkbox = screen.getByRole('checkbox');
    
    // 初期状態の確認
    expect(checkbox).not.toBeChecked();
    
    // クリック後の状態確認
    fireEvent.click(checkbox);
    expect(checkbox).toBeChecked();
    
    // 再度クリック後の状態確認
    fireEvent.click(checkbox);
    expect(checkbox).not.toBeChecked();
  });
});

🔴2. レッド:TODO完了切り替えのテストを書く

TODOアイテムの完了状態を切り替える機能のテストを作成します。

components/TodoItem.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import TodoItem from './TodoItem';

describe('TodoItem コンポーネントのテスト', () => {
  const mockTodo = {
    id: 1,
    text: 'テスト用TODO',
    completed: false
  };

  it('TODOアイテムとチェックボックスが表示されること', () => {
    const mockOnToggle = jest.fn();
    render(<TodoItem todo={mockTodo} onToggle={mockOnToggle} />);
    
    expect(screen.getByText('テスト用TODO')).toBeInTheDocument();
    expect(screen.getByRole('checkbox')).toBeInTheDocument();
    expect(screen.getByRole('checkbox')).not.toBeChecked();
  });

  it('チェックボックスクリック時にonToggleが呼ばれること', () => {
    const mockOnToggle = jest.fn();
    render(<TodoItem todo={mockTodo} onToggle={mockOnToggle} />);
    
    const checkbox = screen.getByRole('checkbox');
    fireEvent.click(checkbox);
    
    expect(mockOnToggle).toHaveBeenCalledWith(1);
    expect(mockOnToggle).toHaveBeenCalledTimes(1);
  });
});

🟢3. グリーン:TodoItemコンポーネントの実装

テストをパスさせるためのTodoItemコンポーネントを実装します。

components/TodoItem.tsx
"use client";

import React from 'react';

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

interface TodoItemProps {
  todo: Todo;
  onToggle: (id: number) => void;
}

export default function TodoItem({ todo, onToggle }: TodoItemProps) {
  return (
    <li className="flex items-center space-x-2 p-2 border-b">
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => onToggle(todo.id)}
        className="w-4 h-4"
      />
      <span
        className={`flex-1 ${
          todo.completed 
            ? 'line-through text-gray-500' 
            : 'text-gray-900'
        }`}
      >
        {todo.text}
      </span>
    </li>
  );
}

🛡️4. 回帰テストの重要性

機能を追加した際に、既存の機能が壊れていないことを確認する回帰テストを実行しましょう。

npm test

すべてのテストが通ることを確認します。もし既存のテストが失敗した場合は、 変更によって既存機能に問題が発生したことを意味します。

🔗5. TodoListコンポーネントでの統合

components/TodoList.tsx を更新して完了切り替え機能を統合します。

components/TodoList.tsx
const handleToggleTodo = (id: number) => {
  setTodos(prevTodos =>
    prevTodos.map(todo =>
      todo.id === id
        ? { ...todo, completed: !todo.completed }
        : todo
    )
  );
};

return (
  <div>
    <TodoForm onSubmit={handleAddTodo} />
    <ul>
      {todos.map(todo => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onToggle={handleToggleTodo}
        />
      ))}
    </ul>
  </div>
);

🧪6. 統合テストの追加

components/TodoList.test.tsx に完了切り替えのテストを追加します。

components/TodoList.test.tsx
it('TODOの完了状態を切り替えできること', () => {
  render(<TodoList />);
  
  const input = screen.getByRole('textbox');
  const submitButton = screen.getByRole('button', { name: /todo を追加/i });
  
  // 新しいTODOを追加
  fireEvent.change(input, { target: { value: '切り替えテスト' } });
  fireEvent.click(submitButton);
  
  // チェックボックスを見つけてクリック
  const checkbox = screen.getByRole('checkbox');
  expect(checkbox).not.toBeChecked();
  
  fireEvent.click(checkbox);
  expect(checkbox).toBeChecked();
  
  // テキストに打ち消し線が適用されているか確認
  const todoText = screen.getByText(/切り替えテスト/i);
  expect(todoText).toHaveClass('line-through');
});

💾7. 学習用テスト:LocalStorageの動作確認

将来的にデータの永続化を実装する準備として、LocalStorageの動作を学習用テストで確認します。

__tests__/learning-tests/local-storage.test.tsx
// 学習用テスト:LocalStorageの動作確認
describe('学習用テスト: LocalStorage', () => {
  beforeEach(() => {
    // 各テスト前にLocalStorageをクリア
    localStorage.clear();
  });

  it('LocalStorageにデータを保存・取得できること', () => {
    const testData = [
      { id: 1, text: 'テスト用TODO', completed: false }
    ];
    
    localStorage.setItem('todos', JSON.stringify(testData));
    
    const retrieved = localStorage.getItem('todos');
    const parsed = retrieved ? JSON.parse(retrieved) : [];
    
    expect(parsed).toEqual(testData);
  });

  it('存在しないキーに対してnullを返すこと', () => {
    const result = localStorage.getItem('nonexistent');
    expect(result).toBeNull();
  });
});

🔄8. 回帰テストのパターン

回帰テストでよく使われるパターンを確認します。

__tests__/regression/todo-functionality.test.tsx
describe('回帰テスト: TODO機能', () => {
  it('切り替え機能追加後も既存機能が正常に動作すること', () => {
    render(<TodoList />);
    
    const input = screen.getByRole('textbox');
    const submitButton = screen.getByRole('button', { name: /todo を追加/i });
    
    // 基本的な追加機能
    fireEvent.change(input, { target: { value: '最初のTODO' } });
    fireEvent.click(submitButton);
    
    fireEvent.change(input, { target: { value: '2番目のTODO' } });
    fireEvent.click(submitButton);
    
    // 両方のTODOが表示されることを確認
    expect(screen.getByText(/最初のTODO/i)).toBeInTheDocument();
    expect(screen.getByText(/2番目のTODO/i)).toBeInTheDocument();
    
    // チェックボックスが両方表示されることを確認
    const checkboxes = screen.getAllByRole('checkbox');
    expect(checkboxes).toHaveLength(2);
    
    // 1つ目を完了に切り替え
    fireEvent.click(checkboxes[0]);
    expect(checkboxes[0]).toBeChecked();
    expect(checkboxes[1]).not.toBeChecked();
  });
});

💡学習用テストと回帰テストの考察

この章で実践した手法:

  • 学習用テスト: 不明な技術要素の動作を確認
  • 回帰テスト: 既存機能が新機能追加によって影響を受けていないことを確認
  • 段階的機能追加: 小さな変更を積み重ねて複雑な機能を構築
  • 継続的検証: 各段階でテストを実行して問題の早期発見

TODOリストの更新

  • TODOアイテムの追加機能
  • TODOアイテムの完了切り替え機能
  • • TODOアイテムの削除機能
  • 外部ライブラリの学習用テスト
  • 回帰テスト

🚀コミットとCIの確認

git add .
git commit -m "feat: Add todo completion toggle with learning and regression tests (Chapter 14)"
git push

次の章では、TypeScriptコンパイラとテストの協調について学び、混合タイプの処理を実装します。