本文へスキップ
BecomeCoder

Java MVVMコース · 第4章 Repository ― データアクセスの抽象化 · レッスン15

Mockito ― モックライブラリで手書きFakeを自動化する

ブラウザで完結

導入

前レッスンで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の基本の型(mockwhenthenReturnverify)を確認します。

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のミニチュア版です。このregisterTaskRepositoryaddを正しく呼んでいるかを、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実装します。

実際に動かしてみよう

このレッスンのサンプルは、実務と同じように役割ごとの .java ファイルへ分けてあります。下のエディタは最初からその複数ファイルが入った状態で、上のタブでファイルを切り替えられます。そのまま「▶ 実行」を押せば全ファイルをまとめて解釈して動かせます。@Test の付いたテストがあるレッスンでは、テストメソッドごとに ✅/❌ の一覧(Red/Green)が出るので、まずテストを赤くしてから実装で緑にする、というTDDの回し方をその場で体験できます(本物のJVMではなく、JUnit・Mockitoの主要な書き方まで再現した学習用シミュレータです)。

Java — ブラウザ内で実行(学習用シミュレータ)

Javaの教材サブセットを動かす学習用シミュレータを読み込みます(本物のJVMではなく、動きを再現した軽量な自作エンジンです)。
スクロールして表示された時点でも自動で読み込まれます。