本文へスキップ
BecomeCoder

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

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

ブラウザで完結

導入

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

説明

flowchart LR
    T["既存テスト(変更しない)"] -->|通れば安全| BEFORE["Before: foreachとifの羅列"]
    T -->|同じテストが通れば安全| AFTER["After: LINQのGroupBy"]
    BEFORE -.振る舞いは同じ.-> AFTER
    style T fill:#e1f5fe
  • リファクタリングの定義: 外から見た振る舞いを変えずに、内部の実装だけを整理すること
  • 「テストが1つも変わらず、全部緑のまま」であることが、振る舞いが変わっていない証拠になる
  • 優先度ごとの件数を数えるCountByPriorityをBefore/Afterで比べます
// Before: 動くが、優先度の種類が増えるたびに if を1つ増やす必要がある
public static Dictionary<string, int> CountByPriority(IReadOnlyList<TaskDto> tasks)
{
    var result = new Dictionary<string, int> { ["Low"] = 0, ["Medium"] = 0, ["High"] = 0 };
    foreach (var task in tasks)
    {
        if (task.Priority == "Low") result["Low"]++;
        else if (task.Priority == "Medium") result["Medium"]++;
        else if (task.Priority == "High") result["High"]++;
    }
    return result;
}
  • 動作は正しいが、優先度の選択肢が1つ増えるたびにifを書き足す必要があり、書き忘れるとバグになる
  • IReadOnlyList<TaskDto>GroupByでグループ化すれば、優先度の種類を1つも書かずに集計できる(Afterは演習で実装します)

やってみよう

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

演習

var tasks = new List<TaskDto>
{
    new(Guid.NewGuid(), "牛乳を買う", "Low", false),
    new(Guid.NewGuid(), "レポート提出", "High", false),
    new(Guid.NewGuid(), "掃除する", "Low", false),
    new(Guid.NewGuid(), "会議資料作成", "High", false),
    new(Guid.NewGuid(), "本を読む", "Medium", false),
};

var counts = TaskStats.CountByPriority(tasks);
Assert(counts["Low"] == 2, "Lowは2件");
Assert(counts["Medium"] == 1, "Mediumは1件");
Assert(counts["High"] == 2, "Highは2件");
Console.WriteLine("全テスト成功");

public record TaskDto(Guid Id, string Title, string Priority, bool IsCompleted);

// TODO: 上のBeforeと同じ結果を、ifの羅列を使わずに
//       GroupBy を使ったリファクタリング版で実装してください
public static class TaskStats
{
    ___
}

void Assert(bool c, string n) { if (!c) throw new Exception($"FAIL: {n}"); }
  • 期待される出力: 全テスト成功
ヒント1を見る

tasks.GroupBy(t => t.Priority)で優先度ごとのグループが作れます

ヒント2を見る

public static Dictionary<string, int> CountByPriority(IReadOnlyList<TaskDto> tasks) => tasks.GroupBy(t => t.Priority).ToDictionary(g => g.Key, g => g.Count());

まとめ

  • リファクタリングは「テストを変えずに実装だけを整理する」こと
  • Before/AfterでAssertが1つも変わっていない=外から見た振る舞いは変わっていない証拠
  • GroupByのようなLINQを使うと、選択肢が増えても書き直す箇所を減らせる

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

実際に動かしてみよう

本文のサンプルや演習のコードは、コードブロック右上の「コピー」ボタンでコピーして、下のエディタに貼り付ければそのまま実行できます。

C# — ブラウザ内で実行

ブラウザ内でC#を動かす環境を読み込みます(初回のみ数秒)。
スクロールして表示された時点でも自動で読み込まれます。