導入
前回のテストはInMemoryTaskRepositoryを使いましたが、実務でRepositoryの実体がDB接続を必要とする場合、テストのたびに本物のDBを用意するのは大変です。Mockitoのmock(...)を使い、TaskRepositoryの偽物を差し込んでTaskServiceだけを検証しましょう。
図解
sequenceDiagram
participant Test
participant TaskService
participant Mock as mock(TaskRepository.class)
Test->>TaskService: new TaskService(mock, factory)
Test->>TaskService: add("牛乳を買う", ...)
TaskService->>Mock: findAll()
TaskService->>Mock: add(task)
Test->>Mock: verify(mock).add(any())
説明
mock(Foo.class)は、Fooというインターフェース(またはクラス)を実装した「呼び出しを記録するだけの偽物」を作ります。
TaskRepository repo = mock(TaskRepository.class);
TaskFactory factory = new TaskFactory(() -> "t1");
TaskService service = new TaskService(repo, factory);
service.add("牛乳を買う", Priority.MEDIUM);
verify(repo).add(any()); // repo.add(...) が1回呼ばれたことを確認
TaskServiceのコードは1行も変えず、渡すTaskRepositoryだけを差し替えるfindAll()をスタブしていなければ、モックは戻り値の型(List)に合わせて自動的に空のリストを返す。だから「まだ何もタスクがない」状態を毎回自分で作らなくてよいwhen(repo.findAll()).thenReturn(list)とスタブすれば、「既にこのタスクがある」という状況も自由に作れるverify(repo).add(any())で「本当にRepositoryへ渡されたか」まで検証できる。verify(repo, never()).add(any())なら「1回も呼ばれていない」ことを確認できる
やってみよう
when(repo.findAll()).thenReturn(...)で「既に同じタイトルのタスクがある」状態を作り、addを呼んだときにrepo.add(...)が1回も呼ばれない(never())ことを確認する書き方を、下のテストで練習します。
演習
Domain.java(土台)・TaskService.java(前回完成させたもの、そのまま使います)・TaskServiceMockTest.java(今日のTODO)の3ファイルです。1つ目のテストは完成していますが、2つ目のテストは自分で書きます。
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;
}
public void add(String title, Priority priority) {
for (TaskItem t : repository.findAll()) {
if (t.getTitle().getValue().equals(title)) {
throw new IllegalStateException("同じタイトルのタスクが既にあります");
}
}
repository.add(factory.create(title, priority));
}
public void complete(String id) {
Optional<TaskItem> found = repository.findById(id);
if (found.isPresent()) {
found.get().complete();
}
}
public int remainingCount() {
int count = 0;
for (TaskItem t : repository.findAll()) {
if (!t.isCompleted()) count++;
}
return count;
}
}
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.ArrayList;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
public class TaskServiceMockTest {
@Test
void juufukuTitleGaSudeniAreba_RepositoryNiTsuikaSarezuNiReigaOkoru() {
TaskRepository repo = mock(TaskRepository.class);
TaskFactory factory = new TaskFactory(() -> "t1");
List<TaskItem> found = new ArrayList<>();
found.add(factory.create("牛乳を買う", Priority.MEDIUM));
when(repo.findAll()).thenReturn(found);
TaskService service = new TaskService(repo, factory);
assertThrows(IllegalStateException.class, () -> service.add("牛乳を買う", Priority.LOW));
verify(repo, never()).add(any());
}
@Test
void juufukuShinaiTaskHaRepositoryNiTsuikaSareru() {
// TODO: mock(TaskRepository.class) を使い、
// service.add("洗濯", Priority.LOW) を呼んだあと、
// repo.add(...) が1回呼ばれたことを verify で確認してください
// (findAll() は何もスタブしなくても、既定で空のリストを返します)
}
}
- 期待される結果: 2件のテストがすべて成功
ヒント1を見る
TaskRepository repo = mock(TaskRepository.class); TaskFactory factory = new TaskFactory(() -> "t9"); TaskService service = new TaskService(repo, factory);
ヒント2を見る
service.add("洗濯", Priority.LOW); verify(repo).add(any());
まとめ
mock(Foo.class)は「呼び出しを記録するだけの偽物」を作る。テスト対象(TaskService)のコードは1行も変えなくてよい- スタブしていないメソッドは戻り値の型に合わせた既定値(
Listなら空リストなど)を返す verify(mock).method(...)で「本当に呼ばれたか」、verify(mock, never()).method(...)で「呼ばれなかったこと」まで検証できる
次回: 画面に渡すデータの形、TaskDtoとTaskMapperを作ります。