本文へスキップ
BecomeCoder

Java MVVMコース · 第9章 テストをやり込む ― TDDの総仕上げ · レッスン36

リファクタリングをテストで支える

ブラウザで完結

導入

前回のように新しい仕様を素朴なifの羅列で実装すると、動きはするものの読みにくいコードになりがちです。ここでは先に動く(Green)が読みにくい実装を見せ、同じテストを1つも変えずに、読みやすい実装へリファクタリングする練習をします。

図解

flowchart LR
    T["既存テスト(変更しない)"] -->|通れば安全| BEFORE["Before: if の羅列"]
    T -->|同じテストが通れば安全| AFTER["After: Mapを更新するループ"]
    BEFORE -.振る舞いは同じ.-> AFTER
    style T fill:#e1f5fe

説明

  • リファクタリングの定義: 外から見た振る舞いを変えずに、内部の実装だけを整理すること
  • 「テストが1つも変わらず、全部緑のまま」であることが、振る舞いが変わっていない証拠になる
  • 優先度ごとの件数を数えるcountByPriorityをBefore/Afterで比べます
// Before: 動くが、優先度の種類が増えるたびに if を1つ増やす必要がある
public static Map<String, Integer> countByPriority(List<TaskDto> tasks) {
    Map<String, Integer> counts = new HashMap<>();
    counts.put("低", 0);
    counts.put("中", 0);
    counts.put("高", 0);
    for (TaskDto task : tasks) {
        if (task.priorityLabel().equals("低")) counts.put("低", counts.get("低") + 1);
        else if (task.priorityLabel().equals("中")) counts.put("中", counts.get("中") + 1);
        else if (task.priorityLabel().equals("高")) counts.put("高", counts.get("高") + 1);
    }
    return counts;
}
  • 動作は正しいが、優先度の選択肢が1つ増えるたびにifを書き足す必要があり、書き忘れるとバグになる
  • 「その優先度のラベルをキーに、カウントを1つ増やす」という同じ処理をラベルの種類ぶん繰り返しているだけなので、ifを並べずに1本のループへまとめられる(Afterは演習で実装します)

やってみよう

Beforeの実装を読み、「優先度がもう1種類増えたら、どこを直し忘れそうか」を考えてから演習に進みましょう。

演習

TaskDto.javaは完成済みとしてそのまま使い、今回の担当はTaskStats.javaだけです。

public record TaskDto(String id, String title, String priorityLabel, boolean completed) { }
// Before(テストは通るが、優先度の種類が増えるたびに if を書き足す必要がある):
// public static Map<String, Integer> countByPriority(List<TaskDto> tasks) {
//     Map<String, Integer> counts = new HashMap<>();
//     counts.put("低", 0);
//     counts.put("中", 0);
//     counts.put("高", 0);
//     for (TaskDto task : tasks) {
//         if (task.priorityLabel().equals("低")) counts.put("低", counts.get("低") + 1);
//         else if (task.priorityLabel().equals("中")) counts.put("中", counts.get("中") + 1);
//         else if (task.priorityLabel().equals("高")) counts.put("高", counts.get("高") + 1);
//     }
//     return counts;
// }

// TODO: 上のBeforeと同じ結果を、if の羅列を使わずに
//       (優先度のラベルをキーにMapを1つずつ更新するループで)実装してください
public class TaskStats {
}
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

public class TaskStatsTest {
    @Test
    void yuusendoGotoNoKensuwoSuukei() {
        List<TaskDto> tasks = new ArrayList<>();
        tasks.add(new TaskDto("1", "牛乳を買う", "低", false));
        tasks.add(new TaskDto("2", "レポート提出", "高", false));
        tasks.add(new TaskDto("3", "掃除する", "低", false));
        tasks.add(new TaskDto("4", "会議資料作成", "高", false));
        tasks.add(new TaskDto("5", "本を読む", "中", false));

        Map<String, Integer> counts = TaskStats.countByPriority(tasks);
        assertEquals(2, counts.get("低"), "低は2件");
        assertEquals(1, counts.get("中"), "中は1件");
        assertEquals(2, counts.get("高"), "高は2件");
    }
}
  • 期待される結果: 1件のテストが成功
ヒント1を見る

Map<String, Integer> counts = new HashMap<>(); counts.put("低", 0); counts.put("中", 0); counts.put("高", 0);のようにあらかじめキーを用意しておきます

ヒント2を見る

for (TaskDto task : tasks) { String label = task.priorityLabel(); counts.put(label, counts.get(label) + 1); }

まとめ

  • リファクタリングは「テストを変えずに実装だけを整理する」こと
  • Before/AfterでassertEqualsが1つも変わっていない=外から見た振る舞いは変わっていない証拠
  • 「同じ処理を繰り返しているifの羅列」は、繰り返しの元になっている値(ここではラベル)をキーにしたループへまとめられることが多い

次回: FakeではなくReal同士を組み合わせて検証する「統合テスト」です。

実際に動かしてみよう

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

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

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