導入
アプリが育つと、DB(または本物のファイル保存)を使うInMemoryTaskRepositoryだけではテストがつらくなります。DBの起動待ち・後片付け・実行順で結果が変わる不安定さ……。そこでテスト専用の偽物(Fake)を用意します。
図解
flowchart LR
T["TaskRegistrationService(テスト対象)"] --> I["TaskRepository(抽象)"]
I -->|本番| R["InMemoryTaskRepository<br/>(実際に保持)"]
I -->|テスト| F["FakeTaskRepository<br/>(挙動を固定・呼び出しを記録)"]
style F fill:#e8f5e9
説明
考え方はこのあとのレッスンで学ぶMoq相当のライブラリMockitoと同じです。Mockitoがやっていることを、このTaskRepositoryに対して手で書きます。
// 手動フェイク: 本物のように振る舞うが、中身は単純な List だけ
public class FakeTaskRepository implements TaskRepository {
private final List<TaskItem> tasks = new ArrayList<>();
private int addCallCount = 0; // 「何回呼ばれたか」を記録できるのがFakeの強み
public void add(TaskItem task) {
tasks.add(task);
addCallCount++;
}
public List<TaskItem> findAll() { return new ArrayList<>(tasks); }
public Optional<TaskItem> findById(String id) {
for (TaskItem t : tasks) {
if (t.getId().equals(id)) return Optional.of(t);
}
return Optional.empty();
}
public int getAddCallCount() { return addCallCount; }
}
- 速い: メモリ上の
Listだけなので、DB接続やファイルI/Oを待たない - 決定的: 毎回同じ結果になる(DBの残留データに左右されない)
- DBが無くても書ける: サーバー担当が実装を書く前でも、Fakeさえあればテストを書き始められる
- おまけに
addCallCountのように「呼ばれた回数」まで記録できる(Mockitoのverifyに相当)
やってみよう
FakeTaskRepositoryに、findByIdが呼ばれた回数を数えるfindByIdCallCountも足して、何回検索されたか確認してみましょう。
演習
Domain.java(ドメインの型)/TaskRepository.java(契約)/FakeTaskRepository.java(未実装)/FakeTaskRepositoryTest.java(テスト)の4ファイルに分けます。
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;
}
public String getValue() { return value; }
public String toString() { return value; }
public boolean equals(Object o) {
if (!(o instanceof TaskTitle)) return false;
return value.equals(((TaskTitle) o).value);
}
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; }
public boolean equals(Object o) {
if (!(o instanceof TaskItem)) return false;
return id.equals(((TaskItem) o).id);
}
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 class SequentialIdGenerator implements IdGenerator {
private int seq = 0;
public String next() {
seq++;
return "t-" + seq;
}
}
public interface TaskRepository {
void add(TaskItem task);
List<TaskItem> findAll();
Optional<TaskItem> findById(String id);
}
// TODO: TaskRepository を実装し、add が呼ばれた回数を getAddCallCount() で
// 数えられる FakeTaskRepository を実装してください
public class FakeTaskRepository implements TaskRepository {
}
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class FakeTaskRepositoryTest {
@Test
void addGaYobareruTabiniAddCallCountGaFueru() {
FakeTaskRepository fake = new FakeTaskRepository();
TaskFactory factory = new TaskFactory(new SequentialIdGenerator());
fake.add(factory.create("牛乳を買う"));
fake.add(factory.create("卵を買う"));
assertEquals(2, fake.getAddCallCount(), "addが2回呼ばれた");
}
@Test
void tsuikashitaTaskWoFindAllToFindByIdDeShutokuDekiru() {
FakeTaskRepository fake = new FakeTaskRepository();
TaskFactory factory = new TaskFactory(new SequentialIdGenerator());
TaskItem t1 = factory.create("牛乳を買う");
fake.add(t1);
fake.add(factory.create("卵を買う"));
assertEquals(2, fake.findAll().size(), "2件保持している");
assertEquals("牛乳を買う", fake.findById(t1.getId()).get().getTitle().getValue(), "IDで取得できる");
}
}
- 期待される結果: 2件のテストがすべて成功
ヒント1を見る
public class FakeTaskRepository implements TaskRepository { private final List<TaskItem> tasks = new ArrayList<>(); private int addCallCount = 0; public void add(TaskItem task) { tasks.add(task); addCallCount++; } public List<TaskItem> findAll() { return new ArrayList<>(tasks); } public Optional<TaskItem> findById(String id) { for (TaskItem t : tasks) { if (t.getId().equals(id)) return Optional.of(t); } return Optional.empty(); } public int getAddCallCount() { return addCallCount; } }
ヒント2を見る
addの中でtasks.add(task)のあとにaddCallCount++を忘れずに
まとめ
- Fakeは「速い・決定的・DB不要」でテストを書けるようにする手動の偽物
TaskRepositoryを実装する点は本物と同じなので、テスト対象からは区別がつかない- 呼び出し回数のような「検証」用の情報も自由に足せる
次回: 手で書いたFakeTaskRepositoryを、モックライブラリMockitoに置き換えます。