本文へスキップ
BecomeCoder

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

テストしにくいコードの見分け方 ― 静的依存とnewの直書き

ブラウザで完結

導入

このコースを通じてTaskServiceが常にTaskRepositoryをコンストラクタで受け取ってきたのは、テストのために最初から意図された設計でした。最後に、その逆――テストが書きにくいコードに共通する特徴を見分けられるようにします。自分で新しいコードを書くときに同じ落とし穴を避けられるようになるのが、この章、そしてこの章の締めくくりです。

図解

flowchart TB
    A["クラスの中でnewを直書き<br/>new ConsoleWriter()"] --> R1["→ コンストラクタで受け取る(DI)"]
    B["現在時刻・IDの発行に<br/>直接依存"] --> R2["→ Clock・IdGeneratorのような<br/>interfaceへ抽象化"]
    C["1つのstaticフィールドに<br/>状態を持たせる"] --> R3["→ インスタンスの状態にする"]
    style A fill:#ffebee
    style B fill:#ffebee
    style C fill:#ffebee

説明

  • newの直書き: メソッドやコンストラクタの中でnew 具体クラス()していると、テストではその具体クラスを避けて通れない。コンストラクタインジェクション(前の章)で外から渡せるようにすれば、Fakeに差し替えられる
  • 静的な非決定値への依存: 現在時刻やランダムな値をロジックの中で直接取得すると、実行するたびに結果が変わり、テストで期待値を固定できない。IdGeneratorClock00-index.mdの共通契約)のように抽象化して注入する
  • staticな可変状態: staticなフィールドに値を貯めていくクラスは、あるテストの実行結果が別のテストに影響してしまう(テストの実行順で結果が変わる、という壊れやすさの温床になる)。状態はインスタンスのフィールドに持たせる

やってみよう

下の演習の「Before」(コメントアウトされたコード)を読み、それが上の3つの観点のどれに当てはまるか考えてから、テストしやすい形に直してみましょう。

演習

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

interface Writer {
    void write(String message);
}
class ConsoleWriter implements Writer {
    public void write(String message) {
        System.out.println(message);
    }
}
// テスト専用。標準出力に出さず、書き込まれた内容を記録するだけ
class FakeWriter implements Writer {
    private final List<String> messages = new ArrayList<>();
    public void write(String message) {
        messages.add(message);
    }
    List<String> getMessages() {
        return messages;
    }
}
// Before(テストしにくい): クラスの内部で具体クラスを直接 new している
// class TaskNotifier {
//     void send(String message) {
//         Writer writer = new ConsoleWriter();   // ← 直書き。テストでは標準出力に出さず検証したい
//         writer.write("[通知] " + message);
//     }
// }

// TODO: Writer をコンストラクタで受け取る(内部で new を直書きしない)ように
//       TaskNotifier を実装してください
class TaskNotifier {
}
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

public class TaskNotifierTest {
    @Test
    void sendDeFakeWriterNiNaiyouGaKakikomareru() {
        FakeWriter fakeWriter = new FakeWriter();
        TaskNotifier notifier = new TaskNotifier(fakeWriter);
        notifier.send("牛乳を買う が完了しました");

        assertEquals(1, fakeWriter.getMessages().size(), "1件書き込まれる");
        assertEquals("[通知] 牛乳を買う が完了しました", fakeWriter.getMessages().get(0), "内容が正しい");
    }
}
  • 期待される結果: 1件のテストが成功
ヒント1を見る

private final Writer writer; TaskNotifier(Writer writer) { this.writer = writer; }

ヒント2を見る

void send(String message) { writer.write("[通知] " + message); }

まとめ

  • newの直書き・静的な非決定値・staticな可変状態の3つが、テストしにくさの主な原因
  • TaskServiceTaskListViewModelが最初から抽象への依存とコンストラクタインジェクションを徹底していたのは、この3つを避けるためだった
  • ここまで積み上げたTDDとMVVMの実践は、次章でアプリの仕上げへとつながる

次章: 全層を振り返り、アプリを完成させる第10章です。

実際に動かしてみよう

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

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

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