本文へスキップ
BecomeCoder

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

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

ブラウザで完結

導入

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

説明

flowchart TB
    A["クラスの中でnewを直書き<br/>new ConsoleWriter()"] --> R1["→ コンストラクタで受け取る(DI)"]
    B["現在時刻・乱数・ID生成に<br/>staticのまま直接依存"] --> R2["→ 第8章のIIdGeneratorのようにinterfaceへ"]
    C["1つのstaticフィールドに<br/>状態を持たせる"] --> R3["→ インスタンスの状態にする"]
    style A fill:#ffebee
    style B fill:#ffebee
    style C fill:#ffebee
  • newの直書き: メソッドやコンストラクタの中でnew 具体クラス()していると、テストではその具体クラスを避けて通れない。コンストラクタインジェクション(第4章)で外から渡せるようにすれば、Fakeに差し替えられる
  • 静的な非決定値への依存: DateTime.NowGuid.NewGuid()new Random()をロジックの中で直接呼ぶと、実行するたびに結果が変わり、テストで期待値を固定できない。第8章のIIdGeneratorのように抽象化して注入する
  • staticな可変状態: staticなフィールドに値を貯めていくクラスは、あるテストの実行結果が別のテストに影響してしまう(テストの実行順で結果が変わる、という壊れやすさの温床になる)。状態はインスタンスのフィールドに持たせる

やってみよう

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

演習

// Before(テストしにくい): クラスの内部で具体クラスを直接 new している
// class TaskNotifier
// {
//     public void Notify(string message)
//     {
//         var writer = new ConsoleWriter();   // ← 直書き。テストではConsoleに出さず検証したい
//         writer.Write($"[通知] {message}");
//     }
// }

var fakeWriter = new FakeWriter();
var notifier = new TaskNotifier(fakeWriter);
notifier.Notify("牛乳を買う が完了しました");

Assert(fakeWriter.Messages.Count == 1, "1件書き込まれる");
Assert(fakeWriter.Messages[0] == "[通知] 牛乳を買う が完了しました", "内容が正しい");
Console.WriteLine("全テスト成功");

interface IWriter { void Write(string message); }
class ConsoleWriter : IWriter { public void Write(string message) => Console.WriteLine(message); }

// テスト専用。Consoleに出さず、書き込まれた内容を記録するだけ
class FakeWriter : IWriter
{
    public List<string> Messages { get; } = new();
    public void Write(string message) => Messages.Add(message);
}

// TODO: IWriter をコンストラクタで受け取る(内部でnewを直書きしない)ように
//       TaskNotifier を実装してください
class TaskNotifier
{
    ___
}

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

private readonly IWriter _writer; public TaskNotifier(IWriter writer) => _writer = writer;

ヒント2を見る

public void Notify(string message) => _writer.Write($"[通知] {message}");

まとめ

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

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

実際に動かしてみよう

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

C# — ブラウザ内で実行

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