🔒第4章 TODOアイテムのプライベート化(意図を語るテスト、TypeScript版)
前章で等価性が定義できたので、テストがより「雄弁」になりました。この章では、TODOアイテムの内部構造をプライベート化し、テストをより「意図を語る」ものに改善します。
作成したばかりの機能を使って、テストを改善した。 そもそも正しく検証できていないテストが2つあったら、もはやお手上げだと気づいた。 そのようなリスクを受け入れて先に進んだ。 テスト対象オブジェクトの新しい機能を使い、テストコードとプロダクトコードの間の結合度を下げた。
TODOリスト
- •
TODOアイテムのフィールドをプライベートにする
- •
テストを等価性で比較するように改善する
- •
操作の意図をより明確にする
🔍1. 現在の実装の問題点
前章で作成したTODOアイテムは、等価性の実装が完了しています。現在の実装を確認してみましょう。
export class TodoItem {
constructor(
public id: number, // publicで直接アクセス可能
public text: string, // publicで直接アクセス可能
public completed: boolean = false
) {}
equals(other: TodoItem): boolean {
// IDとテキストが同じときのみ等価とする
return this.id === other.id && this.text === other.text;
}
}
この実装は等価性の基本機能は動作しますが、さらに改善できる点があります:
- フィールドのプライベート化:現在は
public
で外部から直接変更可能 - 意図を明確にするメソッドの追加:
isCompleted()
、complete()
など - テストの改善:内部構造ではなく振る舞いに焦点を当てたテスト
- 型安全性の向上:nullチェックやinstanceofチェック
const todo = new TodoItem(1, 'TypeScriptでTDDを学ぶ');
// 直接フィールドにアクセス・変更が可能(プライベート化したい)
console.log(todo.id); // 1
console.log(todo.text); // 'TypeScriptでTDDを学ぶ'
console.log(todo.completed); // false
// さらに危険:直接変更もできてしまう
todo.id = 999; // ❌ IDを変更
todo.text = '別の内容'; // ❌ 内容を変更
todo.completed = true; // ❌ 状態を変更
// 型安全性の問題もある
todo.equals(null); // ❌ エラーが発生する可能性
// テストでも直接フィールドを参照(意図を語るテストに改善したい)
expect(todo.id).toBe(1);
expect(todo.text).toBe('TypeScriptでTDDを学ぶ');
expect(todo.completed).toBe(false);
この実装は等価性の基本機能は動作しますが、さらに改善できる点があります:
- フィールドのプライベート化:現在は
public
で外部から直接変更可能 - 意図を明確にするメソッドの追加:
isCompleted()
、complete()
など - テストの改善:内部構造ではなく振る舞いに焦点を当てたテスト
- 型安全性の向上:nullチェックやinstanceofチェック
equals(other: TodoItem): boolean {
return this.id === other.id && this.text === other.text;
}
// 問題:nullやundefinedが渡されるとエラーになる
const todo = new TodoItem(1, 'テスト');
todo.equals(null); // ❌ TypeError: Cannot read property 'id' of null
🔒2. 「プライベート化」とは?
💡 プライベート化の基本概念
プライベート化とは、クラスの内部データを外部から直接アクセスできないようにすることです。 代わりに、必要な操作だけをメソッドとして公開します。
✅ 外部に公開する機能(public)
// これらは外から使ってほしい
todo.isCompleted() // 完了状態の確認
todo.complete() // 完了にする
todo.toggle() // 状態を切り替える
todo.equals(other) // 等価性の比較
✅ これらのメソッドは意図が明確で、安全に使用できます
🎯 なぜプライベート化が重要なのか
1. 🛡️ 意図しない変更を防ぐ
現在の実装では、外部から直接フィールドを変更できてしまい、オブジェクトが壊れる可能性があります。
const todo = new TodoItem(1, '買い物');
// ❌ 現在は直接変更できてしまう(危険)
todo.id = 999; // IDが勝手に変更される
todo.text = '全然違う内容'; // 内容が勝手に変更される
todo.completed = true; // 状態が勝手に変更される
console.log(todo.id); // 999 - 元のIDが失われた
console.log(todo.text); // '全然違う内容' - 元の内容が失われた
プライベート化後は、このような直接変更ができなくなり、安全になります。
2. 🎯 使い方を明確にする
どの機能を使ってほしいかが明確になり、コードが使いやすくなります。
const todo = new TodoItem(1, '買い物');
// ✅ 意図が明確な操作方法
if (todo.isCompleted()) { // 完了状態の確認
console.log('完了済み');
}
const completedTodo = todo.complete(); // 完了にする
const toggledTodo = todo.toggle(); // 状態を切り替える
3. 🔧 将来の変更に強くなる
内部実装を変更しても、外部のコードを壊さずに済みます。
// 内部実装を変更しても...
class TodoItem {
private _id: number;
private _createdAt: Date; // 新しく追加
private _priority: number; // 新しく追加
private _tags: string[]; // 新しく追加
// 外から使う部分(public API)は変わらない
isCompleted(): boolean { /* ... */ }
complete(): TodoItem { /* ... */ }
toggle(): TodoItem { /* ... */ }
}
📝 まとめ
プライベート化 = 内部データを外から直接触れないようにすること
目的 = 安全で使いやすく、変更に強いコードを作ること
方法 = 必要な機能だけをメソッドとして公開する
💬3. 「意図を語るテスト」って何?
🤔 現在のテストの問題点
現在のテストは等価性を中心に書かれていますが、もし内部フィールドに直接アクセスするテストがあったとしたら、以下のような問題があります:
// ❌ 内部の詳細に依存したテスト
it('TODOアイテムが正しく作成されること', () => {
const todo = new TodoItem(1, '買い物');
expect(todo.id).toBe(1); // 内部フィールドに直接アクセス
expect(todo.text).toBe('買い物'); // 内部フィールドに直接アクセス
expect(todo.completed).toBe(false); // 内部フィールドに直接アクセス
});
何が問題なの?
- • 内部構造に依存:フィールド名が変わったらテストが壊れる
- • 意図が不明確:「何をテストしたいのか」が分からない
- • 実装の詳細をテスト:「どう動くか」ではなく「どう作られているか」をテスト
✅ 「意図を語るテスト」とは?
「意図を語るテスト」とは、「何をしたいのか」「どんな振る舞いを期待するのか」が明確に分かるテストのことです。
❌ 意図が不明確
// これだと何をテストしたいのか不明
expect(todo.completed).toBe(false);
✅ 意図が明確
// 「未完了かどうか確認したい」が明確
expect(todo.isCompleted()).toBe(false);
// または
expect(todo.isIncomplete()).toBe(true);
🔄 テストの改善例
実際にテストを改善してみましょう:
❌ 改善前:内部フィールドに依存
it('TODOアイテムが作成されること', () => {
const todo = new TodoItem(1, '買い物');
expect(todo.id).toBe(1);
expect(todo.text).toBe('買い物');
expect(todo.completed).toBe(false);
});
✅ 改善後:意図が明確
it('新しく作成されたTODOは未完了状態であること', () => {
const todo = new TodoItem(1, '買い物');
expect(todo.isCompleted()).toBe(false);
expect(todo.isIncomplete()).toBe(true);
});
it('指定した内容でTODOアイテムが作成されること', () => {
const todo = new TodoItem(1, '買い物');
const expected = new TodoItem(1, '買い物', false);
expect(todo.equals(expected)).toBe(true);
});
改善のポイント
- • テスト名が具体的:「何を確認したいか」が明確
- • メソッドを使用:
isCompleted()
で意図が明確 - • 振る舞いに焦点:「どう動くか」をテスト
次のセクションでは、実際にプライベート化された実装と、それに対応する「意図を語るテスト」を作成していきます。
🛠️4. TODOアイテムの段階的な改善
現在の実装を基に、段階的に改善していきます。まず、フィールドをプライベート化し、意図を明確にするメソッドを追加しましょう。
// 1. インターフェースで構造を明確化
export interface ITodoItem {
readonly id: number;
readonly text: string;
readonly completed: boolean;
}
// 2. クラスでインターフェースを実装
export class TodoItem implements ITodoItem {
private readonly _id: number;
private readonly _text: string;
private readonly _completed: boolean;
constructor(id: number, text: string, completed: boolean = false) {
this._id = id;
this._text = text;
this._completed = completed;
}
// ゲッター(読み取り専用)
get id(): number {
return this._id;
}
get text(): string {
return this._text;
}
get completed(): boolean {
return this._completed;
}
// 意図を明確にするメソッド
isCompleted(): boolean {
return this._completed;
}
isIncomplete(): boolean {
return !this._completed;
}
// 等価性確認(IDとテキストが同じか)
equals(other: TodoItem): boolean {
// 型チェックで安全性を確保
if (!(other instanceof TodoItem)) {
return false;
}
// IDとテキストが同じときのみ等価とする
return this._id === other._id && this._text === other._text;
}
// 不変性を保つメソッド(新しいインスタンスを返す)
toggle(): TodoItem {
return new TodoItem(this._id, this._text, !this._completed);
}
// 完了にする(新しいインスタンスを返す)
complete(): TodoItem {
return new TodoItem(this._id, this._text, true);
}
// 未完了にする(新しいインスタンスを返す)
markIncomplete(): TodoItem {
return new TodoItem(this._id, this._text, false);
}
// 文字列表現
toString(): string {
const status = this._completed ? '✓' : '○';
return `${status} #${this._id}: ${this._text}`;
}
// ファクトリメソッド
static create(id: number, text: string): TodoItem {
return new TodoItem(id, text, false);
}
static createCompleted(id: number, text: string): TodoItem {
return new TodoItem(id, text, true);
}
}
✅ 実装のポイント
- • 新しいTODO追加機能:
TodoItem.create()
ファクトリメソッドを活用 - • 統一されたインターフェース:
onUpdate
一つで全ての状態変更を処理 - • 意図を明確にするメソッドを活用:
isCompleted()
,isIncomplete()
を使用 - • 等価性確認メソッドの活用:
equals()
でアイテムの同一性(IDとテキスト)を判定 - • 不変性の維持:
toggle()
,complete()
で新しいインスタンスを返す - • 型安全性:TypeScriptの型システムを活用
- • ユーザビリティ:Enterキーでの追加、空文字列の入力防止
🧪5. 改善されたテストの実装
📋 元のシンプルなテスト(3章終了時点)
3章終了時点では、基本的な等価性のみをテストしていました:
import { TodoItem } from './todoItem';
describe('TodoItemの等価性テスト', () => {
it('IDとテキストが同じ場合は等価であること', () => {
const todo1 = new TodoItem(1, 'TypeScriptでTDDを学ぶ');
const todo2 = new TodoItem(1, 'TypeScriptでTDDを学ぶ');
expect(todo1.equals(todo2)).toBe(true);
});
it('IDが異なる場合は等価でないこと', () => {
const todo1 = new TodoItem(1, 'TypeScriptでTDDを学ぶ');
const todo2 = new TodoItem(2, 'TypeScriptでTDDを学ぶ');
expect(todo1.equals(todo2)).toBe(false);
});
it('テキストが異なる場合は等価でないこと', () => {
const todo1 = new TodoItem(1, 'TypeScriptでTDDを学ぶ');
const todo2 = new TodoItem(1, 'TODOアプリを構築する');
expect(todo1.equals(todo2)).toBe(false);
});
});
元のテストの問題点
- • 基本的な等価性のみ:IDとテキストの比較だけ
- • 完了状態の考慮なし:完了状態が異なる場合のテストがない
- • 型安全性の未考慮:null/undefinedや異なる型との比較テストがない
- • 意図を語るメソッドのテストなし:
isCompleted()
などのテストがない - • 不変性のテストなし:状態変更時の不変性確認がない
✅ 改善された包括的なテスト(現在)
プライベート化と意図を明確にするメソッドの追加に合わせて、テストも大幅に改善されました:
import { TodoItem } from './todoItem';
describe('TodoItemのテスト', () => {
describe('作成', () => {
it('指定された内容でTODOアイテムを作成すること', () => {
const todo = new TodoItem(1, 'TypeScriptでTDDを学ぶ');
const expected = new TodoItem(1, 'TypeScriptでTDDを学ぶ', false);
expect(todo.equals(expected)).toBe(true); // 等価性メソッドを活用
});
it('デフォルトで未完了のTODOアイテムを作成すること', () => {
const todo = new TodoItem(1, 'TypeScriptでTDDを学ぶ');
expect(todo.isCompleted()).toBe(false); // 意図を明確にするメソッドを使用
expect(todo.isIncomplete()).toBe(true);
});
});
describe('完了状態の操作', () => {
it('TODOアイテムを完了状態にすること', () => {
const todo = new TodoItem(1, 'TypeScriptでTDDを学ぶ');
const completedTodo = todo.complete();
expect(completedTodo.isCompleted()).toBe(true);
expect(todo.isCompleted()).toBe(false); // 不変性の確認
});
it('完了状態を切り替えること', () => {
const todo = new TodoItem(1, 'TypeScriptでTDDを学ぶ');
const toggledTodo = todo.toggle();
expect(toggledTodo.isCompleted()).toBe(true);
expect(todo.isCompleted()).toBe(false); // 不変性の確認
});
});
describe('等価性', () => {
it('IDとテキストが同じ場合は等価であること(完了状態も同じケース)', () => {
const todo1 = new TodoItem(1, 'TypeScriptでTDDを学ぶ', false);
const todo2 = new TodoItem(1, 'TypeScriptでTDDを学ぶ', false);
expect(todo1.equals(todo2)).toBe(true);
});
it('完了状態が異なっても、IDとテキストが同じなら等価であること', () => {
const todo1 = new TodoItem(1, 'TypeScriptでTDDを学ぶ', false);
const todo2 = new TodoItem(1, 'TypeScriptでTDDを学ぶ', true);
expect(todo1.equals(todo2)).toBe(true);
});
it('IDが異なる場合は等価でないこと', () => {
const todo1 = new TodoItem(1, 'TypeScriptでTDDを学ぶ');
const todo2 = new TodoItem(2, 'TypeScriptでTDDを学ぶ');
expect(todo1.equals(todo2)).toBe(false);
});
it('テキストが異なる場合は等価でないこと', () => {
const todo1 = new TodoItem(1, 'TypeScriptでTDDを学ぶ');
const todo2 = new TodoItem(1, 'TODOアプリを構築する');
expect(todo1.equals(todo2)).toBe(false);
});
});
describe('型安全性', () => {
it('nullとの比較で例外が発生しないこと', () => {
const todo = new TodoItem(1, 'テスト');
expect(() => todo.equals(null as any)).not.toThrow();
expect(todo.equals(null as any)).toBe(false);
});
it('undefinedとの比較で例外が発生しないこと', () => {
const todo = new TodoItem(1, 'テスト');
expect(() => todo.equals(undefined as any)).not.toThrow();
expect(todo.equals(undefined as any)).toBe(false);
});
it('異なる型のオブジェクトとの比較で例外が発生しないこと', () => {
const todo = new TodoItem(1, 'テスト');
const fakeObject = { id: 1, text: 'テスト' };
expect(() => todo.equals(fakeObject as any)).not.toThrow();
expect(todo.equals(fakeObject as any)).toBe(false);
});
});
describe('ファクトリメソッド', () => {
it('createメソッドで未完了のTODOを作成すること', () => {
const todo = TodoItem.create(1, 'TypeScriptでTDDを学ぶ');
expect(todo.isIncomplete()).toBe(true);
});
it('createCompletedメソッドで完了済みのTODOを作成すること', () => {
const todo = TodoItem.createCompleted(1, 'TypeScriptでTDDを学ぶ');
expect(todo.isCompleted()).toBe(true);
});
});
});
🔄 主な改善点
1. 📝 テスト構造の改善
- • describeでテストをカテゴリ別に整理
- • 作成、完了状態の操作、等価性、型安全性、ファクトリメソッドに分類
- • テストの意図が明確になり、保守しやすくなった
2. 🎯 意図を語るテストの実装
- • isCompleted()、isIncomplete()メソッドを使用
- • 内部フィールドではなく、振る舞いをテスト
- • テスト名が具体的で、何を確認したいかが明確
3. 🔒 等価性テストの改善
- • 完了状態が異なってもIDとテキストが同じであれば等価であることを確認
- • equals()メソッドがアイテムの同一性(IDとテキスト)を確認することをテスト
- • より実用的な等価性の定義
4. 🛡️ 型安全性テストの追加
- • null、undefined、異なる型との比較テスト
- • 例外が発生しないことを確認
- • 本番環境での予期しないエラーを防止
5. 🏭 ファクトリメソッドテストの追加
- • TodoItem.create()、TodoItem.createCompleted()のテスト
- • オブジェクト作成の意図を明確にするメソッドの動作確認
- • より表現力豊かなAPI
6. 🔄 不変性テストの追加
- • テスト数:3個
- • カバー範囲:基本的な等価性のみ
- • テストカテゴリ:1個(等価性)
📊 テスト数の比較
元のテスト(3章終了時点)
- • テスト数:3個
- • カバー範囲:基本的な等価性のみ
- • テストカテゴリ:1個(等価性)
改善後のテスト(現在)
- • テスト数:13個
- • カバー範囲:全機能を包括的にテスト
- • テストカテゴリ:5個(作成、操作、等価性、型安全性、ファクトリ)
✅ 実装のポイント
- • 新しいTODO追加機能:
TodoItem.create()
ファクトリメソッドを活用 - • 統一されたインターフェース:
onUpdate
一つで全ての状態変更を処理 - • 意図を明確にするメソッドを活用:
isCompleted()
,isIncomplete()
を使用 - • 等価性確認メソッドの活用:
equals()
で更新対象を特定 - • 不変性の維持:
toggle()
,complete()
で新しいインスタンスを返す - • 型安全性:TypeScriptの型システムを活用
- • ユーザビリティ:Enterキーでの追加、空文字列の入力防止
⚛️6. Reactコンポーネントでの使用改善
改良されたTODOアイテムクラスを活用して、Reactコンポーネントを実装します。 ベストプラクティスに基づいた統一されたインターフェースで設計しましょう。
TodoItemComponentの実装
改良されたTodoItemクラスを活用したコンポーネントを作成します:
"use client";
import React from 'react';
import { TodoItem } from '@/lib/todoItem';
interface TodoItemComponentProps {
todoItem: TodoItem;
onUpdate: (updatedTodo: TodoItem) => void;
}
export default function TodoItemComponent({
todoItem,
onUpdate
}: TodoItemComponentProps) {
const handleToggle = () => {
onUpdate(todoItem.toggle());
};
const handleComplete = () => {
if (todoItem.isIncomplete()) {
onUpdate(todoItem.complete());
}
};
const handleMarkIncomplete = () => {
if (todoItem.isCompleted()) {
onUpdate(todoItem.markIncomplete());
}
};
return (
<li className={`flex items-center justify-between space-x-2 p-3 border rounded-lg ${
todoItem.isCompleted()
? 'bg-green-50 border-green-200 text-green-800'
: 'bg-white border-gray-200'
}`}>
<div className="flex items-center space-x-3">
<input
type="checkbox"
checked={todoItem.isCompleted()}
onChange={handleToggle}
className="form-checkbox h-5 w-5 text-green-600"
/>
<span className={`${todoItem.isCompleted() ? 'line-through' : ''}`}>
{todoItem.toString()}
</span>
</div>
<div className="flex space-x-2">
{todoItem.isIncomplete() && (
<button
onClick={handleComplete}
className="px-3 py-1 text-sm bg-green-500 text-white rounded hover:bg-green-600"
>
完了
</button>
)}
{todoItem.isCompleted() && (
<button
onClick={handleMarkIncomplete}
className="px-3 py-1 text-sm bg-gray-500 text-white rounded hover:bg-gray-600"
>
未完了に戻す
</button>
)}
</div>
</li>
);
}
TodoListコンポーネントの実装
等価性メソッドを活用した親コンポーネントを実装します:
"use client";
import React, { useState } from 'react';
import TodoItemComponent from '@/components/todo/TodoItemComponent';
import { TodoItem, ITodoItem } from '@/lib/todoItem';
interface TodoListProps {
initialTodos: ITodoItem[];
}
export default function TodoList({ initialTodos }: TodoListProps) {
const [todos, setTodos] = useState<TodoItem[]>(
initialTodos.map(todo => new TodoItem(todo.id, todo.text, todo.completed))
);
const [newTodoText, setNewTodoText] = useState('');
const handleTodoUpdate = (updatedTodo: TodoItem) => {
setTodos(prevTodos =>
prevTodos.map(todo =>
todo.equals(updatedTodo) ? updatedTodo : todo
)
);
};
const handleAddTodo = () => {
if (newTodoText.trim()) {
const newId = Math.max(0, ...todos.map(todo => todo.id)) + 1;
const newTodo = TodoItem.create(newId, newTodoText.trim());
setTodos(prevTodos => [...prevTodos, newTodo]);
setNewTodoText('');
}
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
handleAddTodo();
}
};
return (
<div className="w-full max-w-2xl mx-auto">
{/* 新しいTODO追加フォーム */}
<div className="mb-6 flex gap-2">
<input
type="text"
value={newTodoText}
onChange={(e) => setNewTodoText(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="新しいTODOを入力..."
className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
<button
onClick={handleAddTodo}
disabled={!newTodoText.trim()}
className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 disabled:bg-gray-300 disabled:cursor-not-allowed"
>
追加
</button>
</div>
{/* TODOリスト */}
{todos.length > 0 ? (
<ul className="w-full space-y-2">
{todos.map((todo) => (
<TodoItemComponent
key={todo.id}
todoItem={todo}
onUpdate={handleTodoUpdate}
/>
))}
</ul>
) : (
<p className="text-gray-500 text-center py-8">No todos yet!</p>
)}
</div>
);
}
ページでの使用例
実際のページでの使用例です:
import TodoList from '@/components/todo/TodoList';
import { ITodoItem, TodoItem } from '@/lib/todoItem';
export default function HomePage() {
const initialTodos: ITodoItem[] = [
{ id: 1, text: 'TypeScriptでTDDを学ぶ', completed: false },
{ id: 2, text: 'Reactコンポーネントを作成する', completed: false },
{ id: 3, text: 'プライベート化を理解する', 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>
<TodoList initialTodos={initialTodos} />
</div>
</main>
);
}
✅ 実装のポイント
- • 新しいTODO追加機能:
TodoItem.create()
ファクトリメソッドを活用 - • 統一されたインターフェース:
onUpdate
一つで全ての状態変更を処理 - • 意図を明確にするメソッドを活用:
isCompleted()
,isIncomplete()
を使用 - • 等価性確認メソッドの活用:
equals()
で更新対象を特定 - • 不変性の維持:
toggle()
,complete()
で新しいインスタンスを返す - • 型安全性:TypeScriptの型システムを活用
- • ユーザビリティ:Enterキーでの追加、空文字列の入力防止
🧪7. Reactコンポーネントのテスト
改良されたTODOアイテムクラスを使ったテスト `TodoItemComponent.test.tsx` も実装しましょう。
コンポーネントテスト
import { render, screen, fireEvent } from '@testing-library/react';
import TodoItemComponent from './TodoItemComponent';
import { TodoItem } from '@/lib/todoItem';
describe('TodoItemComponentのテスト', () => {
it('未完了のTODOアイテムを表示すること', () => {
const todo = TodoItem.create(1, 'TypeScriptでTDDを学ぶ');
const onUpdate = jest.fn();
render(<TodoItemComponent todoItem={todo} onUpdate={onUpdate} />);
expect(screen.getByText(/TypeScriptでTDDを学ぶ/)).toBeInTheDocument();
expect(screen.getByRole('checkbox')).not.toBeChecked();
expect(screen.getByText('完了')).toBeInTheDocument();
});
it('完了済みのTODOアイテムを表示すること', () => {
const todo = TodoItem.createCompleted(1, 'TypeScriptでTDDを学ぶ');
const onUpdate = jest.fn();
render(<TodoItemComponent todoItem={todo} onUpdate={onUpdate} />);
expect(screen.getByRole('checkbox')).toBeChecked();
expect(screen.getByText('未完了に戻す')).toBeInTheDocument();
});
it('チェックボックスクリックでonUpdateが正しい値で呼ばれること', () => {
const todo = TodoItem.create(1, 'TypeScriptでTDDを学ぶ');
const onUpdate = jest.fn();
render(<TodoItemComponent todoItem={todo} onUpdate={onUpdate} />);
fireEvent.click(screen.getByRole('checkbox'));
// onUpdateが呼ばれ、引数が正しいことを確認
expect(onUpdate).toHaveBeenCalledTimes(1);
const calledWith = onUpdate.mock.calls[0][0];
expect(calledWith.isCompleted()).toBe(true);
expect(calledWith.equals(todo)).toBe(true);
});
it('完了ボタンクリックでonUpdateが呼ばれること', () => {
const todo = TodoItem.create(1, 'TypeScriptでTDDを学ぶ');
const onUpdate = jest.fn();
render(<TodoItemComponent todoItem={todo} onUpdate={onUpdate} />);
fireEvent.click(screen.getByText('完了'));
expect(onUpdate).toHaveBeenCalledTimes(1);
const calledWith = onUpdate.mock.calls[0][0];
expect(calledWith.isCompleted()).toBe(true);
});
it('未完了に戻すボタンクリックでonUpdateが呼ばれること', () => {
const todo = TodoItem.createCompleted(1, 'TypeScriptでTDDを学ぶ');
const onUpdate = jest.fn();
render(<TodoItemComponent todoItem={todo} onUpdate={onUpdate} />);
fireEvent.click(screen.getByText('未完了に戻す'));
expect(onUpdate).toHaveBeenCalledTimes(1);
const calledWith = onUpdate.mock.calls[0][0];
expect(calledWith.isCompleted()).toBe(false);
});
it('意図を明確にするメソッドが正しく動作すること', () => {
const incompleteTodo = TodoItem.create(1, 'テスト');
const completedTodo = TodoItem.createCompleted(2, 'テスト2');
// 未完了のTODO
expect(incompleteTodo.isCompleted()).toBe(false);
expect(incompleteTodo.isIncomplete()).toBe(true);
// 完了済みのTODO
expect(completedTodo.isCompleted()).toBe(true);
expect(completedTodo.isIncomplete()).toBe(false);
});
});
統合テストの提案
TodoListコンポーネントの統合テストも作成できます:
import { render, screen, fireEvent } from '@testing-library/react';
import TodoList from './TodoList';
import { ITodoItem } from '@/lib/todoItem';
describe('TodoListの統合テスト', () => {
const initialTodos: ITodoItem[] = [
{ id: 1, text: 'TypeScriptでTDDを学ぶ', completed: false },
{ id: 2, text: 'Reactコンポーネントを作成する', completed: false },
{ id: 3, text: 'プライベート化を理解する', completed: true },
];
it('TODOリストが正しく表示されること', () => {
render(<TodoList initialTodos={initialTodos} />);
expect(screen.getByText(/TypeScriptでTDDを学ぶ/)).toBeInTheDocument();
expect(screen.getByText(/Reactコンポーネントを作成する/)).toBeInTheDocument();
expect(screen.getByText(/プライベート化を理解する/)).toBeInTheDocument();
});
it('チェックボックスクリックで実際に状態が変更されること', () => {
render(<TodoList initialTodos={initialTodos} />);
// 未完了のTODOのチェックボックスをクリック
const checkboxes = screen.getAllByRole('checkbox');
const incompleteCheckbox = checkboxes.find(cb => !cb.checked);
fireEvent.click(incompleteCheckbox!);
// 状態が更新されることを確認(チェックボックスがチェックされる)
expect(incompleteCheckbox).toBeChecked();
});
it('空のリストの場合は適切なメッセージを表示すること', () => {
render(<TodoList initialTodos={[]} />);
expect(screen.getByText('No todos yet!')).toBeInTheDocument();
});
});
テストのポイント
- • 統一されたインターフェース:
onUpdate
一つで全ての操作をテスト - • ファクトリメソッドの活用:
TodoItem.create()
,TodoItem.createCompleted()
を使用 - • 意図を明確にするメソッドのテスト:
isCompleted()
,isIncomplete()
を検証 - • 不変性の確認:
equals()
で異なるインスタンスであることを確認 - • ユーザビリティ:チェックボックスやボタンクリックの動作をテスト
🏃8. テストの実行
すべてのテストを実行して、改良された実装が正しく動作することを確認しましょう。
npm test
すべてのテストがパスすることを確認してください。
🚀9. コミットとCIの確認
git add .
git commit -m "feat: Privatize TodoItem fields and improve intention-revealing tests"
git push
GitHub ActionsのCIワークフローが自動的に実行され、テストがパスすることを確認してください。
🎯この章のまとめ
この章では以下を学びました:
- • 意図を語るテスト:等価性を使った内部構造に依存しないテスト
- • プライベート化:フィールドをプライベートにしてカプセル化を強化
- • ファクトリメソッド:オブジェクト作成の意図を明確にする
- • 不変オブジェクト:状態変更時に新しいインスタンスを返す設計
- • 型安全性の向上:TypeScriptの型システムを活用したAPI設計
次の章では、TODOリストの管理機能を実装し、複数のTODOアイテムを扱う方法を学びます。