本文へスキップ
BecomeCoder

Java MVVMコース · 第10章 仕上げ ― アプリを完成させる · レッスン40

機能追加をTDDで ― 期限切れの強調表示

ブラウザで完結

導入

「締切を過ぎたタスクを目立たせてほしい」という要望が来たとします。実務でこの手の機能追加をするときも、進め方はこれまでと同じです。「期限切れかどうか」の判定はドメイン(TaskItem)の責務としてTDDで先にテストし、ViewModelやViewはその結果(true/false)を受け取って表示を変えるだけ、という責務の分け方を崩さないことが大切です。

図解

flowchart LR
    D["dueDay(締切の通し番号)<br/>と clock.today()(現在)"] --> J["isOverdue(Clock)<br/>(TaskItemが持つ)"]
    J --> V["View / ViewModel<br/>(結果を表示するだけ)"]
    style J fill:#e1f5fe

説明

00-index.mdの拡張仕様どおり、TaskItemlong dueDay(0は期限なし)とisOverdue(Clock clock)を足します。日付はjava.timeを使わず、Clock#today()が返す通し番号(1日を1とする単純な整数)で表します。

Clock clock = () -> 20L;   // 「今日」は20日目
  • dueDay既存のコンストラクタを変えずdueDayを受け取る新しいコンストラクタを1つ追加する形で足します。既存のコンストラクタはthis(...)でこの新しいコンストラクタに委譲すれば、dueDay = 0(期限なし)として扱えます
  • isOverdue(Clock clock)は「期限が設定されていて(dueDay != 0)、まだ完了しておらず、締切(dueDay)が現在(clock.today())より前」ならtrueを返します
  • Clockを引数として受け取るのは、第9章で確認した「静的な非決定値への依存を避ける」ための設計です。テストでは() -> 20Lのように、好きな「今日」を差し込めます

やってみよう

まず何も書かずに「▶ 実行」を押し、isOverduedueDayが無くてコンパイルエラーになる(Red)ことを確認してから演習に進みましょう。

演習

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

public interface Clock {
    long today();   // 「今日」を通し番号(1日=1)で返す
}
// --- 前の章までに作ったクラスのコピー ---
final class TaskTitle {
    private final String value;

    TaskTitle(String value) {
        if (value == null || value.isBlank()) {
            throw new IllegalArgumentException("タイトルは空にできません");
        }
        if (value.length() > 50) {
            throw new IllegalArgumentException("タイトルは50文字以内です");
        }
        this.value = value;
    }

    String getValue() { return value; }
    public String toString() { return value; }
}

enum Priority {
    LOW("低"), MEDIUM("中"), HIGH("高");

    private final String label;
    Priority(String label) { this.label = label; }
    String getLabel() { return label; }
}

// TODO: dueDay(0は期限なし)を受け取るコンストラクタと、isOverdue(Clock) を実装してください。
//       既存の3引数コンストラクタは変更禁止(dueDay=0として、新しいコンストラクタに委譲してください)
class TaskItem {
    private final String id;
    private TaskTitle title;
    private final Priority priority;
    private boolean completed;

    TaskItem(String id, TaskTitle title, Priority priority) {
        this.id = id;
        this.title = title;
        this.priority = priority;
    }

    String getId() { return id; }
    TaskTitle getTitle() { return title; }
    Priority getPriority() { return priority; }
    boolean isCompleted() { return completed; }
    void complete() { completed = true; }
}
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

public class TaskItemTest {
    @Test
    void kigenwoSugitaraOverdueNiNaru() {
        Clock clock = () -> 20L;
        TaskItem task = new TaskItem("1", new TaskTitle("レポート提出"), Priority.HIGH, 10L);
        assertTrue(task.isOverdue(clock), "締切(10日目)を過ぎた20日目はoverdue");
    }

    @Test
    void kigenMaeHaOverdueDenai() {
        Clock clock = () -> 5L;
        TaskItem task = new TaskItem("2", new TaskTitle("企画書"), Priority.MEDIUM, 10L);
        assertFalse(task.isOverdue(clock), "締切(10日目)前の5日目はoverdueではない");
    }

    @Test
    void kigenNashiHaOverdueNiNaranai() {
        Clock clock = () -> 100L;
        TaskItem task = new TaskItem("3", new TaskTitle("いつかやる"), Priority.LOW);
        assertFalse(task.isOverdue(clock), "dueDayが0(期限なし)ならoverdueにならない");
    }

    @Test
    void kanryouZumiHaOverdueNiNaranai() {
        Clock clock = () -> 20L;
        TaskItem task = new TaskItem("4", new TaskTitle("提出済み"), Priority.HIGH, 10L);
        task.complete();
        assertFalse(task.isOverdue(clock), "完了済みならoverdueにならない");
    }
}
  • 期待される結果: 4件のテストが成功
ヒント1を見る

TaskItem(String id, TaskTitle title, Priority priority) { this(id, title, priority, 0); }のように、既存のコンストラクタから新しいコンストラクタへthis(...)で委譲します

ヒント2を見る

boolean isOverdue(Clock clock) { return dueDay != 0 && !completed && dueDay < clock.today(); }

まとめ

  • 新機能を追加するときも「判定ロジックはドメインでTDD」「ViewModel/Viewは結果を表示するだけ」という責務分担は崩さない
  • 既存のコンストラクタは変更せず、this(...)で新しいコンストラクタに委譲すれば、呼び出し側のコードを壊さずに項目を追加できる
  • Clockを引数として渡す設計のおかげで、「今日」を差し替えるだけで境界値(締切当日・前日・翌日など)を何パターンでも画面なしにテストできる

次回: 「カテゴリ分け」機能を、同じ進め方でもう1つ追加します。

実際に動かしてみよう

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

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

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