自動テストについて思っていることを雑に書いてみる
なぜテストコードを書くのか
自動テストは主に「コードを変更した際の予期せぬ不具合を防ぐ」「各コンポーネントが期待通り動くことを保証する」という目的を達成するためにあると思っている(設計のカナリアなどは一旦ここでは除外)。 有効なテスト を書くことで、新規コードの不具合を未然に防ぎ、既存コードを安全に変更できるようになり、結果としてアウトプットの質とスピードが上がる。手法はさておき、これらの目的を満たすテストを書くことが重要だ。
内側のテストと外側のテスト
内側のテスト(単体テスト)を書くだけではリグレッションテストができないため、外側のテスト(インテグレーションテスト・E2Eテスト)も書いたほうが良い。たとえば、あるAPIエンドポイントの一部の処理を担うクラス・メソッドの変更を安全に行うにはインテグレーションテストが書かれていないとAPIとしての動作保証ができない。内側のテストと外側のテストの検証内容が一致するのであれば外側のテストだけ書くほうが保守も考えるとコスパが良い。もちろん、境界値テストなど処理パターンの複雑さによっては外側のテストだけではデータの用意が煩雑になったりテスト時間が伸びる可能性があるため、その場合は内側のテストでカバーすると良い。
モック
外側・内側どちらのテストにおいてもモックは避けたほうが良い。モックは開発者の自作自演のテストになり、リグレッションテストの効果が薄れる。さらに、モックするメソッドの変更に追従する必要があり壊れやすいテストになるため保守性が下がる。モックしない場合はテストデータなどを事前に用意する必要があるが、今どきのフルスタックフレームワークであれば苦労せずテストデータの作成ができるはずだ(テストのしやすさで技術選定するべきとも言える)。むしろ、アプリケーションはデータが中心なので、データを用意しなければ本質的なテストはできない。DBアクセスなどでテスト時間が伸びがちだが、並列化やCIでの自動テストの対象を限定するなどやりようは色々ある。フィードバックの速さが開発者体験的に大事というのは理解できるが、だからといってモックにしてしまうのは本来の目的を見失っていると思う。そういうわけで、モックはHTTPリクエストなどコンテナ等で用意できない要素に絞るなど最小限の利用にすべきだ。
privateメソッドのテスト
内側のテストと外側のテストと考え方は同じで、privateメソッド(内側)をテストしたい場合、ロジックが単純であればpublicメソッド(外側)経由のテストで十分だ。privateメソッドのロジックが複雑で、publicメソッドのテストでやるにはコストがかかるようなケースでは単体テスト(内側のテスト)を行うようにprivateメソッドをテストしても良い。ただし、別クラスにメソッドを適切に移譲できないかどうかは検討する必要がある。ドメインモデル貧血になっていないかは注意が必要で、privateメソッドの処理は概ね既存モデルに移管できることも少なくはなさそう。
テストのために可視性を緩めるかどうか問題
前モデルにも移譲しづらく、クラス分けするほどでもない場合はprivateメソッドのテストをするのは良いと思う。テストをする際はリフレクションを使ってもよいが、可視性を上げて呼び出せるようにしても概ね問題ないように思う。
Java(Guava)やFlutterにはVisibleForTestingアノテーションがあり、テスト用に可視性を上げていることが一目でわかるようにできるし、静的解析でテスト以外で呼び出している場合に警告を出すことも可能だ。
ちなみにPHP(PHPStan)でも同様のアノテーションを作ってみたので良ければ使ってみてほしい。 https://github.com/tzmfreedom/phpstan-visible-for-testing
privateメソッドのテストは壊れやすい?
privateメソッドは公開インターフェースじゃないので、privateメソッドを削除したりインターフェースを変更したり振る舞いを変えたらテストも大幅に変更する必要がある。
テストしたいprivateメソッドがあるということはそのドメインに対して重要なロジックが含まれているということだ。単純なロジックであればpublicメソッド経由でテストすれば良いし、複雑なロジックだからこそprivateメソッドに分割してテストをしたいということだ。その複雑で重要なドメインロジックを有するprivateメソッドを変更するというのはそれなりに大工事をしているわけで、その周辺のテストが壊れるのは想定内であるべきだしテストが落ちること自体は良いことだ。また、もともとのprivateメソッドのテスト自体は「内部テスト」として価値を出していたのではないだろうか。
privateメソッドをモックをしなければインターフェースやメソッドの削除をしても既存の外側のテストは壊れないし、偽陰性・偽陽性は発生しない。
責務分離=クラス新規作成をすべきかどうか問題
privateメソッドをテストしたくなったら、別のクラスを新規作成して、そのクラスにメソッドを実装し呼び出す(移譲する)べきという主張があると思う。これはドメインモデル貧血を防ぐためにも概ね同意ではあるもののモデルに属しづらい処理がある場合、毎回クラスを新規作成するとそれはそれで読み手も書き手もやりづらくないかという気持ちもある。また、クラス新規作成したところで結局同じメソッドが場所を移動しただけになるので、保守性・可読性を考えてリファクタリングを選択すべき。
余談: PHP/LaravelのFacade
LaravelのFacadeって嫌われがちだが、モックしやすいので良い選択だと思っている。 例えばHTTP Facadeを使ってくれれば結合テストを後から書くのがめっちゃ楽。逆にcurlを直接使っていたり、Guzzleをその場でインスタンス生成していたりすると大変。良い感じにDIしてくれれば良いだが、現実はそんなに甘くない…。
まとめ
とにかく 有効なテスト を書くことが大事。 経験上、有効なテストが十分に書かれていないアプリケーションはとても多い。体感7割以上。 レガシーコードと言われているアプリケーションのほとんどがテストが書かれていない。 テストがないからコードを安全に変更できない、安全に変更できないからミドルウェア・ライブラリがアップデートできず、顧客に価値提供ができない、といった形だ。また、書かれていてもモックだらけだったりして真にリグレッションテストができていないコードに出くわすことも少なくない。
テスト時間が遅くなる、テストの保守性が悪い(壊れやすいテストがある)という悩みは本当に幸せな悩みだと思う。テストが無いアプリケーションはテストを増やして防波堤を作るまでの量的な壁があるし、テストを書く文化を根付かせるのはより難易度が高い。だからこそエンジニアにはハードスキルとしてテストを書くスキルを磨いてほしいと思っている。テスト駆動開発じゃなくても全く問題ない。生成AIだとテスト書くのめっちゃ楽だし。原理主義に陥らずテストを書く経験を増やすことが大事。