導入
前回のように新しい仕様を素朴な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同士を組み合わせて検証する「統合テスト」です。