導入
このコースを通じて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に差し替えられる- 静的な非決定値への依存: 現在時刻やランダムな値をロジックの中で直接取得すると、実行するたびに結果が変わり、テストで期待値を固定できない。
IdGenerator・Clock(00-index.mdの共通契約)のように抽象化して注入する - staticな可変状態:
staticなフィールドに値を貯めていくクラスは、あるテストの実行結果が別のテストに影響してしまう(テストの実行順で結果が変わる、という壊れやすさの温床になる)。状態はインスタンスのフィールドに持たせる
やってみよう
下の演習の「Before」(コメントアウトされたコード)を読み、それが上の3つの観点のどれに当てはまるか考えてから、テストしやすい形に直してみましょう。
演習
Writer.java・ConsoleWriter.java・FakeWriter.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つが、テストしにくさの主な原因TaskService・TaskListViewModelが最初から抽象への依存とコンストラクタインジェクションを徹底していたのは、この3つを避けるためだった- ここまで積み上げたTDDとMVVMの実践は、次章でアプリの仕上げへとつながる
次章: 全層を振り返り、アプリを完成させる第10章です。