nunulkのプログラミング徒然日記

毎日暇なのでプログラミングの話題について書きたいことがあれば書いていきます。

私がプライベートメソッドに対するテストを書く理由

はじめに

この記事について

表題のとおりなんですが、すべて書いているわけではなくて、必要だと思ったときには書いている、というのが正確な表現です。 書くべきか?と問われると No なんですが、書かないべきか?と問われるとそれも No で、書きたければ書く、がいまのところ自分のポリシーです。本文でもう少し深掘りします。

文脈

言語は PHP です。 以下のようなシチュエーションを想定しています。

  1. 長大なパブリックメソッドに対して自動テストが書きにくいため、内部で使われているプライベートメソッドに対して自動テストを書きたいシチュエーション
  2. モジュールの外に公開する必要性がないが、入出力のパターンがそれなりにある

1. 長大なパブリックメソッドに対して自動テストが書きにくいため、内部で使われているプライベートメソッドに対して自動テストを書きたいシチュエーション

単純に実行に時間がかかる、あるいは、モジュール分割がうまくいっておらず、ひとつのメソッドの責務が多すぎる、といった理由で内部のプライベートメソッドに対してまず、自動テストを用意することはあります。

たとえば、バッチコマンドで1ヶ月分のなんらかのデータを作成する必要があるとします。その場合、以下のような構造になることがあります。

<?php

class SomeCommand
{
    public function execute(int $year, int $month): void
    {
        $dateRange = DateRange::fromMonth($year, $month);
        // すべての日付分処理すると時間がかかる
        foreach ($dateRange as $date) {
            // ここに長大な処理がある
        }
    }
}

パブリックメソッドのインターフェースとして年と月を受けて、内部では日付分ループしていて実体のほとんどはこのブロックの中にあるとします。それを以下のようにプライベートメソッドに切り出して、そいつに対してユニットテストを書けば、時間を節約できます。

<?php

class SomeCommand
{
    public function execute(int $year, int $month): void
    {
        $dateRange = DateRange::fromMonth($year, $month);
        foreach ($dateRange as $date) {
            // プライベートメソッドに切り出す
            $this->executeForDate($date);
        }
    }

    // このメソッドに対してテストを書く
    private function executeForDate(Date $date): void {}
}

人によっては、以下のようにクラスを分割してパブリックメソッドにしてからテストを書くようにしているかもしれません。

<?php
class SomeCommand
{
    public function __construct(private readonly DailySomeCommand $dailySomeCommand) {}

    public function execute(int $year, int $month): void
    {
        $dateRange = DateRange::fromMonth($year, $month);
        foreach ($dateRange as $date) {
            // DailySomeCommand::execute() に対してテストを書く
            $this->dailySomeCommand->execute($date);
        }
    }
}

私ならこのケースでは、日別の処理が他で必要ないのであればクラス化はしないですが、似たようなケースで分割することもあります。自分の中の分割基準はありますが、それは今回範囲外なので割愛します。

2. モジュールの外に公開する必要性がないが、入出力のパターンがそれなりにある

ある意味では 1 のパターンと同じですが、外部に公開する必要はないが、そのモジュールの内部処理としてはパターンがそれなりにあるのでテストしておきたい、というシチュエーションです。私はテスト駆動で開発することが多いんですが、そういう場合はまず公開メソッドに対してテストを書くことからスタートします。しかし、書いているうちに、関数化したくなるときがあります。 下記の例では、入力パラメーターから論理値を導出して以降の処理で使用するような関数ですが、最初は公開メソッドにすべての処理を書いていきます(関数抽出もクラス抽出もしません)。

<?php

class SomeClass
{
    public function doSomething(array $args)
    {
        $isHoge = false;
        // パターンを導出する処理を書いていく
        if ($args['hoge'] === 'fuga' && /* ほかにも依存関係がある */) {
            $isHoge = true;
        } elseif (...) {
            // ...
        }
        // ... だんだん複雑になってくる
}

論理値の導出ロジックがそれなりに複雑になってくると専用のテストが書きたくなります。そこで、導出ロジックをプライベートメソッドに切り出して、そのプライベートメソッドに対してテストを書くことになります。

<?php
class SomeClass
{
    public function doSomething(array $args)
    {
        $isHoge = $this->isHoge($args, /* ほかにも依存関係がある */);
    }

    // このメソッドに対してテストを書く
    private function isHoge(array $args, /* ... */): bool
    {
        // パターンを導出する処理
    }
}

これも、1 のパターンと同じで、外部クラスに置くこともあります。とくに enum を使う場合、ファクトリメソッドを用意することが多いので、二値であっても enum にしてファクトリメソッドを使うか、上のケースではもしかすると Specification パターンを使ってそちらに置くかもしれません。しかし、入力パラメーターだけでなく SomeClass 固有の依存関係がいくつも絡んでくるようなケースや複数の変数をまとめて導出するような複雑な処理だと、できるだけ内部に閉じていてほしいのでプライベートメソッドにするかもしれません。その辺は用途範囲だったり、複雑さだったりによって、やり方が変わってきます。

プライベートかどうかと関数にユニットテストがほしいかは別問題

モジュール内の関数の可視性はモジュールの都合であって、複雑性とか保守性とかとは関係ないと思っていて、 上に挙げた例でも、仮にプライベートメソッドが担う処理をまるっとクラスに抽出しパブリックメソッドにしたとすると、当然テストを書きますよね?そうなると公開か非公開かというのはテストを書くか書かないかというのとは関係ないはず、と思います。

テストコミュニティ内で非公開関数を直接テストするべきかについては議論があり、 他の言語では非公開関数をテストするのは困難だったり、不可能だったりします。 あなたがどちらのテストイデオロギーを支持しているかに関わらず、Rustの公開性規則により、 非公開関数をテストすることが確かに可能です。

テストの体系化 - The Rust Programming Language 日本語版

さらっとイデオロギーと書いてありますが、私もプライベートメソッドに関する議論はまさにイデオロギーだと思います。プライベートメソッドにテストを書くべき(あるいは書かないべき)というとき、どういう文脈において、どういう悪影響があるのか、というのが分かっているのであれば問題ないですが、とにかく書くべき(あるいは書かないべき)、というのは思考停止かなと思います。

チーム開発においては、チームの方針としてプライベートメソッドに対してテストを書かないべき、という方針であれば(いままで幸いにもそういう現場に入ったことはないですが)、いちおう上のような話をして、それでもなおその方針でいきます、という反応であればそれに従うようにするつもりです。自分にとっては、そこまでして守りたいポリシーではなく、正直どちらでもいいんですが、べき論によって問題が起こったり複雑化したりする場合は、本当にその方針でいいのか、毎回しつこく確認するようにしたいとは思っています。

おわりに

もうちょっと上手いこと言語化できると思っていましたが、思ったよりもふわっとした内容になってしまいました(実際のコードを使えるともう少し具体性が増すんですけどね…)。

いずれにしても、私はこれからもプライベートメソッドにテストを書くことを厭わない、という意思表明だけしてこのエントリーを終えたいと思います。

日頃からどの文脈においても気をつけていることですが、私はなるべく「〜すべき」ではなく「〜すると〜より良い」という考え方をするようにしています。色んな状況や文脈で取れる解決策を選ぶとき、頭から「〜べき」「〜しないべき」と考えて選択肢の幅を狭めてしまうのはもったいないし、問題と向き合わない態度だと思うので、自分自身のポリシーとしては問題と向き合い、解決のために柔軟な思考を持ち続けていく所存です。