導入
「同じタイトルのタスクは2つ登録できない」というルールを、TDDのリズムで実装しましょう。まずRed(失敗するテスト)としてこの仕様を先に書き、それを通すGreen(最小実装)を書きます。
図解
flowchart TB
ADD["add(title, priority)"] --> CHK{"同じタイトルが<br/>既にある?"}
CHK -->|はい| ERR["IllegalStateException"]
CHK -->|いいえ| NEW["factory.create → repository.add"]
style ERR fill:#ffebee
style NEW fill:#e8f5e9
説明
Redから書き始めます。まだ実装していないルールを、先にテストとして固定するのがポイントです。
TaskRepository repo = new InMemoryTaskRepository();
TaskService service = new TaskService(repo, new TaskFactory(() -> "t1"));
service.add("牛乳を買う", Priority.MEDIUM);
// 同じタイトルはもう一度追加できない、というルールを先にテストとして書く
boolean rejected = false;
try {
service.add("牛乳を買う", Priority.LOW);
} catch (IllegalStateException e) {
rejected = true;
}
System.out.println(rejected); // true
- 「Redを書く」とは、まだ実装していない仕様を先にテストとして書くこと
- ここでは「重複したら
IllegalStateExceptionが飛ぶ」という仕様を先に固定した - 次にこれを通す最小実装(Green)を
TaskServiceに書く
complete(id)は、存在しないidを渡されても例外にせず何もしません。repository.findById(id)がOptionalを返すので、isPresent()で確認してからcomplete()を呼びます。
やってみよう
下のエディタで、completeに存在しないidを渡した場合の動きも試しながら実装しましょう。何も起きない(例外にならない)のが正しい仕様です。
演習
Domain.java(前章までの土台)・TaskService.java(今日のTODO)・TaskServiceTest.java(テスト)の3ファイルです。
import java.util.List;
import java.util.ArrayList;
import java.util.Optional;
public interface IdGenerator {
String next();
}
public class TaskTitle {
private final String value;
public TaskTitle(String value) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("タイトルは空にできません");
}
if (value.length() > 50) {
throw new IllegalArgumentException("タイトルは50文字以内です");
}
this.value = value.trim();
}
public String getValue() { return value; }
@Override
public String toString() { return value; }
@Override
public boolean equals(Object o) {
if (!(o instanceof TaskTitle)) return false;
return value.equals(((TaskTitle) o).value);
}
@Override
public int hashCode() { return value.hashCode(); }
}
public enum Priority {
LOW("低"), MEDIUM("中"), HIGH("高");
private final String label;
Priority(String label) { this.label = label; }
public String getLabel() { return label; }
}
public class TaskItem {
private final String id;
private TaskTitle title;
private final Priority priority;
private boolean completed;
public TaskItem(String id, TaskTitle title, Priority priority) {
this.id = id;
this.title = title;
this.priority = priority;
}
public String getId() { return id; }
public TaskTitle getTitle() { return title; }
public Priority getPriority() { return priority; }
public boolean isCompleted() { return completed; }
public void complete() { completed = true; }
public void rename(TaskTitle newTitle) { title = newTitle; }
@Override
public boolean equals(Object o) {
if (!(o instanceof TaskItem)) return false;
return id.equals(((TaskItem) o).id);
}
@Override
public int hashCode() { return id.hashCode(); }
}
public class TaskFactory {
private final IdGenerator idGenerator;
public TaskFactory(IdGenerator idGenerator) {
this.idGenerator = idGenerator;
}
public TaskItem create(String title, Priority priority) {
return new TaskItem(idGenerator.next(), new TaskTitle(title), priority);
}
public TaskItem create(String title) {
return create(title, Priority.MEDIUM);
}
}
public interface TaskRepository {
void add(TaskItem task);
List<TaskItem> findAll();
Optional<TaskItem> findById(String id);
}
public class InMemoryTaskRepository implements TaskRepository {
private final List<TaskItem> tasks = new ArrayList<>();
public void add(TaskItem task) {
tasks.add(task);
}
public List<TaskItem> findAll() {
return tasks;
}
public Optional<TaskItem> findById(String id) {
for (TaskItem t : tasks) {
if (t.getId().equals(id)) return Optional.of(t);
}
return Optional.empty();
}
}
import java.util.Optional;
public class TaskService {
private final TaskRepository repository;
private final TaskFactory factory;
public TaskService(TaskRepository repository, TaskFactory factory) {
this.repository = repository;
this.factory = factory;
}
// TODO: 同じタイトルのタスクが既にあれば IllegalStateException を投げる add と、
// Id から探して complete() する complete を実装してください
public void add(String title, Priority priority) {
}
public void complete(String id) {
}
public int remainingCount() {
int count = 0;
for (TaskItem t : repository.findAll()) {
if (!t.isCompleted()) count++;
}
return count;
}
}
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class TaskServiceTest {
@Test
void juufukuTitleHaTsuikaDekizuRepositoryNimoTorokuSarenai() {
TaskRepository repo = new InMemoryTaskRepository();
TaskService service = new TaskService(repo, new TaskFactory(() -> "t1"));
service.add("牛乳を買う", Priority.MEDIUM);
assertThrows(IllegalStateException.class, () -> service.add("牛乳を買う", Priority.LOW));
assertEquals(1, repo.findAll().size(), "重複は登録されない");
}
@Test
void completeDeKanryoFlagGaTatsu() {
TaskRepository repo = new InMemoryTaskRepository();
TaskService service = new TaskService(repo, new TaskFactory(() -> "t2"));
service.add("レポートを書く", Priority.HIGH);
String id = repo.findAll().get(0).getId();
service.complete(id);
assertTrue(repo.findAll().get(0).isCompleted(), "完了フラグが立つ");
assertEquals(0, service.remainingCount(), "未完了は0件");
}
@Test
void sonzaiShinaiIdWoCompleteShitemo_NanimoOkoranai() {
TaskRepository repo = new InMemoryTaskRepository();
TaskService service = new TaskService(repo, new TaskFactory(() -> "t3"));
assertDoesNotThrow(() -> service.complete("nai-id"));
}
}
- 期待される結果: 3件のテストがすべて成功
ヒント1を見る
addはfor (TaskItem t : repository.findAll())で既存のタイトルと見比べ、一致したらthrow new IllegalStateException(...)します。なければrepository.add(factory.create(title, priority))。
ヒント2を見る
completeはOptional<TaskItem> found = repository.findById(id); if (found.isPresent()) found.get().complete();
まとめ
- 「他のデータと見比べる判断」はServiceの仕事(Repositoryは1件ずつの出し入れしか知らない)
- 仕様を先にテスト(Red)として書き、それを通す実装(Green)を書くリズムはServiceでも変わらない
completeは存在しないidならfindByIdがOptional.empty()を返し、isPresent()で何もしない
次回: このテストで、本物のInMemoryTaskRepositoryをMockitoのモックに差し替えます。