💭第7章 疑念をテストに翻訳する(異なるタイプの比較、TypeScript版)
開発を進める中で「これで本当に大丈夫だろうか?」という疑念が生まれることがあります。この章では、そうした疑念をテストコードに翻訳し、確信を得る方法を学びます。
頭の中にある悩みをテストとして表現した。 完璧ではないものの、まずまずのやり方(getClass
)でテストを通した。 さらなる設計は、本当に必要になるときまで先延ばしにすることにした。
感情をテストに翻訳することは、TDDの大きなテーマの1つだ。今回は副作用への不快感を、同じDollar
に何回も掛け算を行うテストとして表現した。
TODOリスト
- •
異なるタイプのアイテム間の比較について疑念を解決する
- •
ItemManagerの型安全性に関する不安を検証する
- •
将来の拡張性についての疑念をテストで確認する
🤔1. 最初の疑念:アイテム管理での型混在
ItemManagerクラスを実装しましたが、異なる型のアイテム(TodoItemとTaskItem)を混在させて管理することに対して疑念が生まれています。
現在の疑念
- • TodoItemとTaskItemが同じコレクションに入っている時、予期しない動作をしないか?
- • 型の判定やキャストが正しく動作するか?
- • パフォーマンスに問題はないか?
🔴2. 疑念をテストに翻訳する
疑念を具体的なテストケースに変換しましょう。
lib/typeConversion.test.ts
import { TodoItem } from './todoItem';
import { TaskItem } from './taskItem';
import { ItemManager } from './itemManager';
import { BaseItem } from './baseItem';
describe('型変換と混在に関する疑念のテスト', () => {
describe('混在型コレクション', () => {
it('配列操作で混在型を適切に処理すること', () => {
const items: BaseItem[] = [
TodoItem.create(1, 'TDDを学ぶ'),
TaskItem.create(2, 'PRをレビューする', 'high'),
TodoItem.create(3, 'ドキュメントを書く'),
TaskItem.create(4, 'バグを修正する', 'low')
];
// 疑念:異なる型が混在したときの操作は安全か?
const completedItems = items.map(item => item.complete());
expect(completedItems).toHaveLength(4);
expect(completedItems.every(item => item.isCompleted())).toBe(true);
// 各アイテムが正しい型を維持しているか
expect(completedItems[0]).toBeInstanceOf(TodoItem);
expect(completedItems[1]).toBeInstanceOf(TaskItem);
expect(completedItems[2]).toBeInstanceOf(TodoItem);
expect(completedItems[3]).toBeInstanceOf(TaskItem);
});
it('型によるフィルタリングが安全に動作すること', () => {
const items: BaseItem[] = [
TodoItem.create(1, 'TODO 1'),
TaskItem.create(2, 'タスク 1', 'high'),
TodoItem.create(3, 'TODO 2'),
TaskItem.create(4, 'タスク 2', 'low')
];
// 疑念:型フィルタリングは正確に動作するか?
const todos = items.filter((item): item is TodoItem => item instanceof TodoItem);
const tasks = items.filter((item): item is TaskItem => item instanceof TaskItem);
expect(todos).toHaveLength(2);
expect(tasks).toHaveLength(2);
// 型安全性の確認:TypeScriptが正しい型を推論しているか
todos.forEach(todo => {
expect(typeof todo.id).toBe('number');
expect(typeof todo.text).toBe('string');
// TaskItem特有のメソッドは呼べないはず(コンパイルエラーになる)
});
tasks.forEach(task => {
expect(typeof task.getPriority()).toBe('string');
expect(task.isHighPriority()).toBeDefined();
});
});
});
describe('型変換のエッジケース', () => {
it('空のコレクションを適切に処理すること', () => {
const emptyItems: BaseItem[] = [];
const todos = emptyItems.filter((item): item is TodoItem => item instanceof TodoItem);
const tasks = emptyItems.filter((item): item is TaskItem => item instanceof TaskItem);
expect(todos).toHaveLength(0);
expect(tasks).toHaveLength(0);
});
it('単一型のコレクションを処理すること', () => {
const onlyTodos: BaseItem[] = [
TodoItem.create(1, 'TODO 1'),
TodoItem.create(2, 'TODO 2')
];
const tasks = onlyTodos.filter((item): item is TaskItem => item instanceof TaskItem);
expect(tasks).toHaveLength(0);
});
});
});
🟢3. ItemManagerの拡張と疑念の解決
ItemManagerを拡張し、型安全性への疑念を解決します。
lib/itemManager.ts(拡張版)
import { BaseItem } from './baseItem';
import { TodoItem } from './todoItem';
import { TaskItem, Priority } from './taskItem';
export interface ItemStatistics {
total: number;
completed: number;
incomplete: number;
todos: number;
tasks: number;
highPriorityTasks: number;
}
export class ItemManager {
private items: BaseItem[] = [];
addItem(item: BaseItem): void {
this.items.push(item);
}
addItems(items: BaseItem[]): void {
this.items.push(...items);
}
removeItem(itemId: number): boolean {
const initialLength = this.items.length;
this.items = this.items.filter(item => item.id !== itemId);
return this.items.length < initialLength;
}
completeItem(itemId: number): boolean {
const itemIndex = this.items.findIndex(item => item.id === itemId);
if (itemIndex === -1) return false;
this.items[itemIndex] = this.items[itemIndex].complete();
return true;
}
toggleItem(itemId: number): boolean {
const itemIndex = this.items.findIndex(item => item.id === itemId);
if (itemIndex === -1) return false;
this.items[itemIndex] = this.items[itemIndex].toggle();
return true;
}
// 型安全なゲッターメソッド
getAllItems(): readonly BaseItem[] {
return [...this.items];
}
getTodos(): TodoItem[] {
return this.items.filter((item): item is TodoItem => item instanceof TodoItem);
}
getTasks(): TaskItem[] {
return this.items.filter((item): item is TaskItem => item instanceof TaskItem);
}
getCompletedItems(): BaseItem[] {
return this.items.filter(item => item.isCompleted());
}
getIncompleteItems(): BaseItem[] {
return this.items.filter(item => item.isIncomplete());
}
getTasksByPriority(priority: Priority): TaskItem[] {
return this.getTasks().filter(task => task.getPriority() === priority);
}
getHighPriorityTasks(): TaskItem[] {
return this.getTasksByPriority('high');
}
// 統計情報の取得
getStatistics(): ItemStatistics {
const todos = this.getTodos();
const tasks = this.getTasks();
return {
total: this.items.length,
completed: this.getCompletedItems().length,
incomplete: this.getIncompleteItems().length,
todos: todos.length,
tasks: tasks.length,
highPriorityTasks: this.getHighPriorityTasks().length
};
}
// 検索機能
findByText(searchText: string): BaseItem[] {
const lowerSearchText = searchText.toLowerCase();
return this.items.filter(item =>
item.text.toLowerCase().includes(lowerSearchText)
);
}
findById(id: number): BaseItem | undefined {
return this.items.find(item => item.id === id);
}
// バッチ操作
completeAllTasks(): void {
this.items = this.items.map(item =>
item instanceof TaskItem ? item.complete() : item
);
}
completeAllTodos(): void {
this.items = this.items.map(item =>
item instanceof TodoItem ? item.complete() : item
);
}
toString(): string {
return this.items.map(item => item.toString()).join('\n');
}
clear(): void {
this.items = [];
}
isEmpty(): boolean {
return this.items.length === 0;
}
}
🔵4. 拡張されたItemManagerのテスト
lib/itemManagerExtended.test.ts
import { ItemManager } from './itemManager';
import { TodoItem } from './todoItem';
import { TaskItem } from './taskItem';
describe('ItemManagerの拡張機能テスト', () => {
let manager: ItemManager;
beforeEach(() => {
manager = new ItemManager();
});
describe('バッチ操作に関する疑念', () => {
it('TODOを保持しながらすべてのタスクを完了すること', () => {
// 疑念:バッチ操作が意図した型にのみ影響するか?
manager.addItems([
TodoItem.create(1, 'TODO 1'),
TaskItem.create(2, 'タスク 1', 'high'),
TodoItem.create(3, 'TODO 2'),
TaskItem.create(4, 'タスク 2', 'low')
]);
manager.completeAllTasks();
const todos = manager.getTodos();
const tasks = manager.getTasks();
expect(todos.every(todo => !todo.isCompleted())).toBe(true);
expect(tasks.every(task => task.isCompleted())).toBe(true);
});
});
describe('統計情報の正確性に関する疑念', () => {
it('混在コレクションで正確な統計情報を提供すること', () => {
// 疑念:統計情報が正確に計算されるか?
manager.addItems([
TodoItem.create(1, 'TODO 1'),
TaskItem.create(2, '高優先度タスク', 'high'),
TodoItem.createCompleted(3, '完了済みTODO'),
TaskItem.create(4, '低優先度タスク', 'low'),
TaskItem.createCompleted(5, '完了済み高優先度タスク', 'high')
]);
const stats = manager.getStatistics();
expect(stats.total).toBe(5);
expect(stats.completed).toBe(2);
expect(stats.incomplete).toBe(3);
expect(stats.todos).toBe(2);
expect(stats.tasks).toBe(3);
expect(stats.highPriorityTasks).toBe(2);
});
});
describe('検索機能に関する疑念', () => {
it('異なるアイテムタイプ間で安全に検索すること', () => {
// 疑念:検索機能が全ての型で正しく動作するか?
manager.addItems([
TodoItem.create(1, 'TypeScriptの基礎を学ぶ'),
TaskItem.create(2, 'TypeScript上級編を学ぶ', 'high'),
TodoItem.create(3, 'JavaScriptを練習する'),
TaskItem.create(4, 'TypeScriptプロジェクトのセットアップ', 'medium')
]);
const results = manager.findByText('TypeScript');
expect(results).toHaveLength(3);
expect(results.some(item => item instanceof TodoItem)).toBe(true);
expect(results.some(item => item instanceof TaskItem)).toBe(true);
});
});
});
🔮5. 将来の拡張性への疑念
新しいアイテムタイプ(例:リマインダーアイテム)を追加することを想定したテストを書きます。
lib/reminderItem.ts(仮想的な新しいタイプ)
import { BaseItem, ItemId, ItemText } from './baseItem';
export class ReminderItem extends BaseItem {
private readonly _dueDate: Date;
constructor(id: ItemId, text: ItemText, dueDate: Date, completed: boolean = false) {
super(id, text, completed);
this._dueDate = dueDate;
}
static create(id: ItemId, text: ItemText, dueDate: Date): ReminderItem {
return new ReminderItem(id, text, dueDate, false);
}
getDueDate(): Date { return this._dueDate; }
isOverdue(): boolean {
return !this._completed && this._dueDate < new Date();
}
toggle(): ReminderItem {
return new ReminderItem(this._id, this._text, this._dueDate, !this._completed);
}
complete(): ReminderItem {
return new ReminderItem(this._id, this._text, this._dueDate, true);
}
markIncomplete(): ReminderItem {
return new ReminderItem(this._id, this._text, this._dueDate, false);
}
withDueDate(dueDate: Date): ReminderItem {
return new ReminderItem(this._id, this._text, dueDate, this._completed);
}
toString(): string {
const status = this._completed ? '✓' : '○';
const dateStr = this._dueDate.toLocaleDateString();
const overdueMarker = this.isOverdue() ? '⚠️' : '';
return `${status} 📅 #${this._id}: ${this._text} (${dateStr}) ${overdueMarker}`;
}
}
lib/extensibility.test.ts
import { ItemManager } from './itemManager';
import { TodoItem } from './todoItem';
import { TaskItem } from './taskItem';
import { ReminderItem } from './reminderItem';
describe('拡張性に関する疑念のテスト', () => {
it('新しいアイテムタイプを追加しても既存機能が動作すること', () => {
// 疑念:新しいアイテムタイプを追加しても既存機能は動作するか?
const manager = new ItemManager();
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
manager.addItems([
TodoItem.create(1, 'TODOアイテム'),
TaskItem.create(2, 'タスクアイテム', 'high'),
ReminderItem.create(3, 'リマインダーアイテム', tomorrow)
]);
// 基本操作が全ての型で動作することを確認
expect(manager.getAllItems()).toHaveLength(3);
// 型フィルタリングが既存型に影響しないことを確認
expect(manager.getTodos()).toHaveLength(1);
expect(manager.getTasks()).toHaveLength(1);
// 新しい型のフィルタリング
const reminders = manager.getAllItems().filter(
(item): item is ReminderItem => item instanceof ReminderItem
);
expect(reminders).toHaveLength(1);
expect(reminders[0].getDueDate()).toEqual(tomorrow);
});
it('新しい型でもポリモーフィックな動作を維持すること', () => {
// 疑念:ポリモーフィズムが新しい型でも正しく動作するか?
const manager = new ItemManager();
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
manager.addItems([
ReminderItem.create(1, 'Overdue reminder', yesterday),
ReminderItem.create(2, 'Future reminder', new Date(Date.now() + 86400000))
]);
// ポリモーフィックな操作
manager.completeItem(1);
const items = manager.getAllItems();
expect(items[0].isCompleted()).toBe(true);
expect(items[1].isCompleted()).toBe(false);
// 型固有の機能
const reminders = items.filter((item): item is ReminderItem => item instanceof ReminderItem);
expect(reminders[0].isOverdue()).toBe(false); // 完了済みなのでオーバーデューではない
expect(reminders[1].isOverdue()).toBe(false); // 期限前
});
});
⚡6. パフォーマンスへの疑念
lib/performanceConcerns.test.ts
import { ItemManager } from './itemManager';
import { TodoItem } from './todoItem';
import { TaskItem } from './taskItem';
describe('パフォーマンスに関する疑念のテスト', () => {
it('大量の混在コレクションを効率的に処理すること', () => {
// 疑念:大量の異なる型のアイテムでもパフォーマンスは維持されるか?
const manager = new ItemManager();
const start = performance.now();
// 10,000個の混合アイテムを作成
const items = [];
for (let i = 0; i < 10000; i++) {
if (i % 3 === 0) {
items.push(TodoItem.create(i, `TODO ${i}`));
} else if (i % 3 === 1) {
items.push(TaskItem.create(i, `タスク ${i}`, 'medium'));
} else {
items.push(TaskItem.create(i, `高優先度タスク ${i}`, 'high'));
}
}
manager.addItems(items);
const addTime = performance.now() - start;
// 操作のパフォーマンステスト
const opStart = performance.now();
const stats = manager.getStatistics();
const highPriorityTasks = manager.getHighPriorityTasks();
const opTime = performance.now() - opStart;
expect(stats.total).toBe(10000);
expect(addTime).toBeLessThan(50); // 50ms以内での追加
expect(opTime).toBeLessThan(100); // 100ms以内での操作
expect(highPriorityTasks.length).toBeGreaterThan(0);
});
it('継承階層での等価性チェックを効率的に維持すること', () => {
// 疑念:継承階層での等価性チェックのパフォーマンスは問題ないか?
const todo1 = TodoItem.create(1, 'テスト');
const todo2 = TodoItem.create(1, 'テスト');
const task = TaskItem.create(1, 'テスト', 'high');
const start = performance.now();
for (let i = 0; i < 100000; i++) {
todo1.equals(todo2); // 同じ型
todo1.equals(task); // 異なる型
}
const duration = performance.now() - start;
expect(duration).toBeLessThan(100); // 100ms以内
});
});
🚀コミットとCIの確認
git add .
git commit -m "feat: Translate concerns into tests and extend ItemManager functionality"
git push
🎯この章のまとめ
この章では以下を学びました:
- • 疑念の具体化:漠然とした不安を具体的なテストケースに翻訳
- • 型安全性の検証:TypeScriptの型システムと実行時の動作の整合性確認
- • 拡張性のテスト:将来の機能追加に対する準備とテスト
- • パフォーマンス懸念の解決:性能に関する疑念を測定可能なテストで検証
- • 設計判断の先延ばし:必要になるまで複雑な設計を避ける判断
TDDにおいて「疑念をテストに翻訳する」ことは、設計上の不安を具体的に解決し、自信を持って開発を進めるための重要な技術です。この手法により、推測ではなく事実に基づいた開発を行うことができます。
これで第1編:基礎編の第2章〜第7章が完了しました。次のステップでは、これらの基礎を元により複雑なTODOアプリケーションの機能を構築していくことになります。