要約
privateメソッドに単体テストを書いてはいけない理由は、端的に言うと、privateメソッドは観察可能な振る舞いではないからです。
観察可能は振る舞いではないものをテストしようとすると、テストが実装の詳細と結びつき、リファクタリング耐性が失われてしまいます。
もしprivateなメソッドがビジネス上重要な役割を担っていて、どうしてもテストしたい場合は、他のクラスとして切り出すなどの処置をすると良いです。
良いテストの基本原則
良いテストの基本原則に 「観察可能な振る舞いのみを検証する」 というものがあります。これは『単体テストの考え方/使い方』の5章で紹介されているものです。
「観察可能な振る舞い」とは
以下の2つを満たすものが観察可能な振る舞いです。
- クライアントが目標を達成するために使われる、公開された「操作」
- クライアントが目標を達成するために使われる、公開された「状態」
例えば、人件費を計算するgetCosts
というメソッドがあるとします。そしてgetCosts
はcalc
、sum
というメソッドを内部で利用しています。
public function getCosts()
{
$tmp = calc();
$result = sum($tmp);
return $result;
}
private function calc()
{
// 何らかの処理
}
private function sum($tmp)
{
// 何らかの処理
}
この時、getCosts
を呼び出しているクライアントからすると、目的を達成するために呼び出しているのはgetCosts
のみであり、calc
やsum
は「観察可能な振る舞い」ではありません。
ここで、getCosts
がクライアントとなってcalc
やsum
を呼び出しているではないか、という指摘が入るかもしれません。もしcalc
やsum
が他のクラスで定義されているpublicメソッドであるなら、その指摘は正しいです。getCosts
はcalc
やsum
からすると、外部(クライアント)であるとみなすことができます。
ですが今回、2つのメソッドはprivateです。privateメソッドは外部から秘匿されたものであり、内部実装(実装の詳細)になります。なので2つのメソッドは観察可能な振る舞いとは言えません。
なぜ観察可能な振る舞いのみテストをするのか
端的に言うと、テストケースが実装の詳細を結びついてしまうからです。
テストケースが実装の詳細と結びつくとは、テストケースが、テスト対象であるものの内部的なコードを扱ってしまうことです。
逆に実装の詳細との結びつきがないテストとは、テスト対象がどのような結果を生み出すのか、ということを検証するものです。テスト対象がどのようにしてその結果を生み出すのかはテストしません。これは、検証時に目を向けるのは 何(What) であり、 どのように(how) ではないと説明されることがあります。
実装の詳細と結びついたテストを実装してしまうと、結果としてリファクタリング耐性を失うことになります。
例えば、getCosts
(コストを取得するメソッド)のテストをするとき、getCosts
が内部で呼び出しているprivateメソッドが何回呼ばれているのかをカウントしたりするのは、まさにテスとケースと実装の詳細が結びついてしまっている例になります(本来はWhatのみを見るべきなので、ここではgetCostsに特定の引数を渡すと、特定の値が常に返ってくる、ということのみをテストするのが良いです)。
これをしてしまうと、getCosts
の内部処理が変更されたとき、テストコードも変更しなくてはならなくなります。つまり、正しくリファクタリングを行えていたとしても、テストが通らなくなる偽陽性の状態になってしまうということです。
これはリファクタリング耐性を失ってしまっていると言えます。
抽象化の欠落
ただ、privateなメソッドがビジネス上重要な役割を担っていて、どうしてもテストしたい場合というのもあるかと思います。
例えば以下のように金額を計算するクラスがあったとします。
class Order
{
public function GenerateDescription()
{
$amount = calc();
return "合計金額は" . $amount . "円です";
}
private function calc()
{
// とても重要で複雑な処理...
}
}
原則に従うならprivateメソッドであるcalcはテストを書いてはいけませんが、calcの処理はとても複雑かつ重要な役割を担っています。
このように、重要なビジネスロジックがprivateメソッドに埋もれてしまうことを抽象化の欠落といます。
抽象化の欠落が起きている場合、privateメソッドは別のクラスに切り出した方がいいです。
class Order
{
public function GenerateDescription()
{
$service = new Calculate();
$amount = $service->calc();
return "合計金額は" . $amount . "円です";
}
// private function calc()
// {
// // とても重要で複雑な処理...
// }
}
class Calculate
{
public function calc()
{
// とても重要で複雑な処理...
}
}
privateなメソッドをテストしたいから別のclassに抽出する、ということは、privateなメソッドを観察可能な振る舞いに変えたという点では、privateをpublicに変えただけのように見えるかもしれません。でもこのような切り出しを行うことで
- インターフェースを適切に用意すればカプセル化を維持できる
- テストが容易になる
- 責任範囲が明確になる
などのメリットがあります。
これは、観察可能は振る舞いを少し増やすには十分なメリットではないのかなと思います。
終わりに
privateなメソッドに単体テストを書くことはアンチパターンではありますが、実はリフレクションなどを使えば、わざわざサービスに切り出さなくてもテストすること自体はできてしまいます。実務ではそのようにせざるを得ない場合もあるのかなと思います。
私自身またまだテストに対する知識や経験がないので、誤り等ございましたらご指摘いただけるとありがたいです。