導入
「今日は何日目か」「新しいIDを発行する」――こうした小さな共通処理をヘルパークラスに切り出すことはよくあります。ただし、これを**staticメソッド**として直接呼ぶか、インターフェースにしてコンストラクタで注入するかは、テストのしやすさに直結する設計判断です。
説明
flowchart LR
subgraph static_["staticで直接呼ぶ"]
S["SystemClock.today()"] -->|直接呼ぶ| X["呼ぶたびに違う値<br/>テストで固定できない"]
end
subgraph di["interface + 注入"]
I["Clock(抽象)"] -.-> Real["本番: 実際の日付"]
I -.-> Fake["テスト: () -> 20L<br/>(決まった値を返すラムダ)"]
end
style Fake fill:#e8f5e9
判断の目安はシンプルです。
- 純粋な計算(入力だけで結果が決まり、副作用がない):
staticのままで十分テストできる。文字列整形や数値計算のようなヘルパーがこれにあたる(レッスン1のTitleRulesがまさにこれ) - 呼ぶたびに結果が変わる/外部の状態に触れる(現在時刻・乱数・新規ID発行・ファイルI/O):
staticのまま直接呼ぶと、テストで結果を固定できない。インターフェースに抽象化してコンストラクタで注入し、テストでは予測可能な値を返すFake(またはラムダ)に差し替える
共通のAPI契約にあるClock・IdGeneratorは、まさにこの2つ目のパターンです。第7章のSequentialIdGeneratorが本番用の実装でしたが、テストではさらに単純に、ラムダでその場限りの実装を作ることもできます。
Clock fixedClock = () -> 20L; // "今日は20日目" というテスト専用のClock
このコースのシミュレータにはjava.timeが無いため、実際の日付を返す本番のClock実装を動かすことはできません。しかし設計そのもの(staticで直接日付を取りに行くのではなく、Clockという抽象を受け取る)は、実務のコードでもまったく同じです。
やってみよう
Clockを受け取ってメッセージを記録するActivityLogを実装し、固定のClockを渡すだけで結果を予測できることを確認してみましょう。
演習
Clock.javaは完成済みとしてそのまま使い、今回の担当はActivityLog.javaだけです。
public interface Clock { long today(); }
import java.util.List;
import java.util.ArrayList;
public class ActivityLog {
private final Clock clock;
private final List<String> entries = new ArrayList<>();
public ActivityLog(Clock clock) {
this.clock = clock;
}
// TODO: "◯日目: メッセージ" という形式の文字列を entries に追加する append を実装してください
// (◯には clock.today() の値を使います)
public void append(String message) {
}
public List<String> getEntries() {
return entries;
}
}
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class ActivityLogTest {
@Test
void kotayonoHizukeGaKirokuNiHairu() {
Clock fixedClock = () -> 20L;
ActivityLog log = new ActivityLog(fixedClock);
log.append("牛乳を買うを追加した");
assertEquals(1, log.getEntries().size(), "1件記録される");
assertEquals("20日目: 牛乳を買うを追加した", log.getEntries().get(0));
}
@Test
void hiGaSusumuToKirokuMoKawaru() {
Clock day1 = () -> 1L;
ActivityLog log1 = new ActivityLog(day1);
log1.append("最初の記録");
Clock day2 = () -> 2L;
ActivityLog log2 = new ActivityLog(day2);
log2.append("別の日の記録");
assertEquals("1日目: 最初の記録", log1.getEntries().get(0));
assertEquals("2日目: 別の日の記録", log2.getEntries().get(0));
}
}
- 期待される結果: 2件のテストがすべて成功
ヒント1を見る
public void append(String message) { entries.add(clock.today() + "日目: " + message); }
ヒント2を見る
clock.today()はlong型なので、文字列と+でつなげればそのまま数字が文字列に変換されます
まとめ
- 副作用のない純粋なヘルパーは
staticのままで十分テストできる - 現在時刻・乱数・新規ID発行のような「呼ぶたびに変わる」ヘルパーはインターフェースに抽象化し、コンストラクタで注入する
- テストでは、本番の実装の代わりにラムダで「決まった値を返すだけの偽物」をその場で作れる(
Clock fixed = () -> 20L;) TaskFactory.createが内部でidGenerator.next()を直接呼んでいるのは、Idの実際の値をテストで検証する必要がない(重複さえしなければよい)からこそ許される設計判断
次回: MVVMでやってしまいがちな失敗、アンチパターンです。