導入
前レッスンでFakeTaskRepositoryを手で書きました。これはTaskRepositoryが1つだけならまだ楽ですが、実務ではインターフェースの数だけ手書きのFakeクラスが必要になり、インターフェースにメソッドを1つ足すたびに全部のFakeを直して回る羽目になります。Mockitoは、この「手書きのFake」をインターフェースからその場で自動生成してくれるモックライブラリです。書くコードはFakeTaskRepositoryというクラス定義ではなく、「このメソッドが呼ばれたらこう返す」という設定だけになります。
図解
flowchart LR
subgraph before["前レッスン: 手書きのFake"]
A["class FakeTaskRepository implements TaskRepository { ... }<br/>を自分でクラスとして書く"]
end
subgraph after["Mockito: その場で自動生成"]
B["mock(TaskRepository.class)<br/>when(...).thenReturn(...)"] --> C["TaskRepository として渡せる<br/>代理オブジェクト"]
end
style after fill:#e8f5e9
説明
Calculatorという小さなインターフェースで、Mockitoの基本の型(mock・when・thenReturn・verify)を確認します。
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
interface Calculator {
int add(int a, int b);
}
public class CalculatorMockitoTest {
@Test
void setupShitaHikisuDeYobarerutoShiteiShitaAtaiWoKaesu() {
Calculator calc = mock(Calculator.class);
when(calc.add(2, 3)).thenReturn(10); // 「add(2, 3)が呼ばれたら10を返す」という設定
int result = calc.add(2, 3); // calc は Calculator の代理オブジェクト
assertEquals(10, result, "whenで設定した戻り値がそのまま返る");
}
@Test
void verifyDeYobaretaKotoWoKakuninDekiru() {
Calculator calc = mock(Calculator.class);
when(calc.add(any(), any())).thenReturn(0);
calc.add(1, 1);
verify(calc, times(1)).add(1, 1); // 「add(1, 1)がちょうど1回呼ばれたか」を検証する
}
}
mock(Calculator.class)だけで、Calculatorを実装した代理オブジェクトがその場で作られる。手でclass Fake... implements Calculator { ... }と書く必要はないwhen(calc.add(2, 3)).thenReturn(10)は「add(2, 3)が呼ばれたら10を返す」という設定。実際にそのメソッドを呼び出す形で「呼び出し方そのもの」を指定できるのがFakeとの一番の違いany()は「どんな引数でもよい」という意味。引数を限定せずに戻り値だけ決めたいときに使うverify(calc, times(1)).add(1, 1)は「指定した呼び出しが指定した回数だけ実際にあったか」を確認する。前レッスンのaddCallCountを手で数える代わりに、Mockitoが自動で数えてくれる
演習
TaskRegistrationServiceは「同じタイトルのタスクがなければ登録する」という、次章のTaskServiceのミニチュア版です。このregisterがTaskRepositoryのaddを正しく呼んでいるかを、Mockitoで検証します。あなたが書くのはTaskRegistrationServiceMockitoTest.javaの1箇所(fail(...)になっている場所)だけです。
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);
}
// 次章で本格的な TaskService を作りますが、ここでは Mockito の練習として
// 同じ形の小さなクラスを使います(同じタイトルのタスクがあれば例外)。
public class TaskRegistrationService {
private final TaskRepository repository;
private final TaskFactory factory;
public TaskRegistrationService(TaskRepository repository, TaskFactory factory) {
this.repository = repository;
this.factory = factory;
}
public TaskItem register(String title, Priority priority) {
for (TaskItem t : repository.findAll()) {
if (t.getTitle().getValue().equals(title)) {
throw new IllegalStateException("同じタイトルのタスクが既にあります");
}
}
TaskItem task = factory.create(title, priority);
repository.add(task);
return task;
}
}
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
public class TaskRegistrationServiceMockitoTest {
@Test
void registerHaRepositoryNoAddWoChodo1KaiYobu() {
TaskRepository mockRepository = mock(TaskRepository.class);
when(mockRepository.findAll()).thenReturn(new ArrayList<>());
TaskRegistrationService service = new TaskRegistrationService(mockRepository, new TaskFactory(new SequentialIdGenerator()));
service.register("牛乳を買う", Priority.MEDIUM);
// TODO: mockRepository に対して、add(...) がちょうど1回呼ばれたことを
// verify で検証してください(引数はどんな TaskItem でもよい = any())
fail("TODO: verify(mockRepository, times(1)).add(any()); を書いてください");
}
@Test
void juufukuTitleGaSudeniAruTokiAddHaYobarenai() {
TaskItem existing = new TaskFactory(new SequentialIdGenerator()).create("牛乳を買う");
List<TaskItem> existingList = new ArrayList<>();
existingList.add(existing);
TaskRepository mockRepository = mock(TaskRepository.class);
when(mockRepository.findAll()).thenReturn(existingList);
TaskRegistrationService service = new TaskRegistrationService(mockRepository, new TaskFactory(new SequentialIdGenerator()));
assertThrows(IllegalStateException.class, () -> service.register("牛乳を買う", Priority.MEDIUM), "重複タイトルは例外になる");
verify(mockRepository, never()).add(any());
}
}
- 期待される結果: 2件のテストがすべて成功
ヒント1を見る
verify(mockRepository, times(1)).add(any());
ヒント2を見る
times(1)は「ちょうど1回」、never()は「1回も呼ばれていない」を検証します。引数の中身までは問わないのでany()を使います
まとめ
- Mockitoは
mock(Foo.class)でインターフェースの代理オブジェクトをその場で自動生成する。class Fake... implements Foo { ... }を手で書く必要がなくなる when(...).thenReturn(...)で「呼ばれたときの戻り値」を、verify(...)で「実際に呼ばれたか・何回呼ばれたか」を検証できるany()のような引数マッチャーで、「引数の値は問わず、呼ばれたかどうかだけ確認したい」という指定もできる- 前レッスンの
FakeTaskRepository(手書き)と今回のmock(TaskRepository.class)(自動生成)は、TaskRegistrationServiceから見れば同じTaskRepository。テスト対象のコードは一切変わらない
次回: TaskRegistrationService自体をコンストラクタインジェクションでTDD実装します。