テスタブルな設計の勘所、副作用を外に出して依存を注入する

テスタブルな設計とは、メソッド内で握り込んだ依存や副作用を外に追い出し、必要なものはコンストラクタから注入し、判断や計算を純粋な部分として切り離す作り方です。

テストしづらいコードには理由がある

「これテスト書けないな」と感じるコードは、たいてい中で勝手に外の世界に触っています。メソッドの奥で new して依存を握ったり、time()rand() をその場で呼んだり。こういう隠れた副作用があると、テストから状況をコントロールできなくなるんですね。テスタブルにするというのは、その「握り込み」を外に追い出す作業だと思っています。

// テストしづらい: 依存を中で作り、現在時刻を直に読む
class Report
{
    public function make(): string
    {
        $db = new Database(); // 差し替え不能
        $now = date('Y-m-d');  // 固定できない
        return $now . ':' . $db->count();
    }
}

依存はコンストラクタから受け取る

まず new を追い出して、必要なものは外から渡してもらう形にします。いわゆる依存性注入ですね。こうしておくと、テストではスタブやモックを渡すだけで済みます。実装を差し替えられる余地を作っておく、というだけの話ですが、これだけでテストのしやすさが段違いになります。

class Report
{
    public function __construct(
        private readonly CountRepository $repo,
        private readonly Clock $clock, // 時刻も抽象化して注入
    ) {}

    public function make(): string
    {
        return $this->clock->today() . ':' . $this->repo->count();
    }
}

副作用と純粋な計算を分ける

もうひとつ効くのが、判断や計算といった「純粋な部分」と、保存や送信といった「副作用の部分」を分けることです。計算部分を副作用から切り離しておくと、そこは入力と出力を確かめるだけで済み、モックすら要らなくなります。副作用は薄い層に押し込めて、中身の判断を純粋関数に寄せる感じですね。

// 純粋: 入力から結果を返すだけ。テストが素直
function shouldNotify(int $stock, int $threshold): bool
{
    return $stock < $threshold;
}

// 副作用は呼び出し側に置く
if (shouldNotify($stock, 10)) {
    $mailer->send(/* ... */);
}

まとめ

テスタブルな設計というと大げさに聞こえますが、やることは「依存を外から渡す」「副作用と計算を分ける」の二つに集約される気がします。テストのために設計を歪めるというより、テストしやすい形は普通に読みやすく壊れにくい、という副産物のほうが本質だと思います。テストを書こうとして手が止まったら、それは設計からのサインだと受け取るようにしています。

よくある質問

Q. 現在時刻や乱数に依存したコードはどうテストしますか?
A. time() や rand() をその場で呼ぶと固定できません。Clock のような抽象に包んで注入すれば、テストでは好きな時刻や値を渡せるようになります。

Q. テストのために設計を歪めることになりませんか?
A. むしろ逆で、依存を外から渡し副作用と計算を分けた形は、普通に読みやすく壊れにくくなります。テストしやすさはその副産物だと考えるといいと思います。

類似投稿

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です