有用なテストケースにおける開発者の心構え

はじめに

今回は、各種コミュニティで定期的に登場する「プライベートメソッドをテストするには、どうすればよいか?」あるいは「プライベートメソッドはテストすべきか?」という質問について考えてみたいと思います。テーマ自体は簡単ですが人によって考え方が異なり、特に海外の開発者の意見は千差万別です。最終的にこの問題は「効果的なテストケースとは何か」という問いに似ています。

プライベートメソッドは、オブジェクト指向の観点から考えられたもので、露出した関数内部にアクセスするクロージャの中に隠された関数も等しく対象となります。モジュールの外部インターフェースの背後に隠されたカプセル化されものを言います。(以下、内部実装と呼ぶ)
内部実装はテストの必要があるでしょうか?内部実装に該当するものは、直接テストケースを作成することは避け、公開された外部インターフェースを通じてのみテストしなければなりません。つまり、テストは必要です。

反復するテストを自動化するため、内部実装のテストケースを「一時的に」作成するのは構いませんが、結局は外部インターフェースをテストするテストケースのみ残す必要があります。

何が重要か

テストとは、テストケースを作成してターゲットモジュールのテストを自動化することを言います。テストによって得られるメリットは枚挙にいとまがありませんが、それについても開発者によって意見が分かれるため、ここではユニットテストや統合テストの区別なく、単にコードで作成され、自動化されたテストを対象にしたいと思います。

テストの目的は、開発者(プロジェクト)をサポートすることです。実際にテストケースをどのように作成するかについては、それを使用する開発者にとって役立つものであれば、どのような形式でも問題ないでしょう。内部実装をテストするかどうかも開発者の自由であり、何が正解であるかを求める必要もありません。そのため、開発者自身の役に立つものであればプライベートメソッドもテストすることができます。しかし、それはあくまで一人で開発するときの話で、大勢で開発するプロジェクトの観点から考えると話が異なります。

また、テスト駆動開発(TDD)のプロセスに従って進行すると、外部公開されるインターフェースだと思っていたものが、実際には内部実装に移されるケースが多くあり、さらにはインターフェースが削除されたり合体したりすることもあります。これらのモジュールの変化に応じて、内部実装を直接検証するテストケースをなくしたり、外部インターフェースのテストの一部として吸収する必要があります。

自己満足の罠

内部実装を直接的にテストすると、テストケースの将来的な価値を落とすことになります。テストケースを作成したところで、これによって取得できる真のメリットが分からないまま、TDDが提示する「レッド/グリーン/リファクタリングサイクル」に閉じ込められ盲目的にテストケースを作成することは避けるべきです。テストケースを作ったからといって、必ずしも役に立つわけではないからです。

自分で試してみると分かりますが、内部実装を直接テストした方が、外部インターフェースを通じてテストするよりもはるかに簡単です。そして、内部実装をテストすることが、あたかも直感的で、馴染みがあり、満足感が得られることもあります。早くグリーンを見たいために、簡単なテストケースを作成したいと思うこともあるかもしれません。このように作成された大量のテストケースを見ると胸がいっぱいになりますが、だからといって無条件にテストケースを作成しても役に立つものではありません。テストケースは少ないほど良く、最小のテストケースで最大の効果を出すのが、テストの価値をより向上させます。逆の場合は、たくさんのテストケースがプロジェクトの敏捷性を落とし、事あるごとに足を引っ張ることになるでしょう。

不要なテストケースが多いほど、実際に有用で必要なテストケースの価値が低下し、プロジェクトでのテストケースに対する信頼度が損なわれます。テストケースは信頼を失った瞬間、レガシーコードよりも劣るプロジェクトの障害になります。

モジュールとテストケースの関係

アプリケーションの中で各自が責任を持ちコラボレーションするモジュールは、一部の機能が変更されたり、性能が良いモジュールと交換されることがあります。一種の巨大な機関の歯車のようなものをイメージするとよいでしょう。

古い歯車は、より軽くて速い安定性の優れた歯車に交換されることがあります。このとき、歯車が所属する機関は、その歯車にかみ合って回転できるように歯の形状のみを理解していればよく、歯車の材質、色、商標などのような情報がなくても問題にはなりません。そのため、新しい歯車を回転させる前にテストすべきことは、その歯車と一緒に回転する他の歯と正しくかみ合うかどうかを確認するだけで十分です。

テストケースは、モジュールの機能が一部変更されたり追加されたとき、既存のシステムで当該モジュールが責任を負うべき役割を忠実に履行できるかを保証します。そして、モジュールの内部構造がリファクタリングされて変更されたり、あるいは全く新しいモジュールに再作成されたとき、アプリケーションと呼ばれる巨大なシステムでは、モジュールがうまくかみ合って動作するか信頼性を確保します。

そのため、テストケースはターゲットモジュールが何であれ、システムにかみ合って正常に戻るかどうかを確認する必要があります。歯車がどのような形態か、内部は空かどうか、固体構造物になっているか、またはマトリョーシカ人形のように歯車の中に歯車があるか、などの情報は関係ありません。歯車のユーザーは歯車に公開された歯の形状のみを理解し、機関で使用できる状態にしなければなりません。それが歯車の役割と責任であり、テストケースもモジュールのユーザーであると言えます。テストケースは変化できる具体的なモジュールではなく、変化しないモジュールの責任をテストしなければなりません。モジュールが適切に責任を実行するように、メッセージを受信する外部インターフェースが頻繁に変更される場合は、それは何かが誤って設計されているというシグナルです。

モジュールは常に内部が異なりますが、同じ外部インターフェースを持つ、すなわち同じ役割と責任を持つモジュールと交換することができ、そのモジュールを使用するテストを行うテストケースでは、当該モジュールの抽象化された責任のみが分かっていれば問題ありません。そのため、モジュールのポリモーフィズムと自律性を確保する必要があります。
これはどこかで見た内容ではありませんか?SOLID原則の依存逆転原則(DIP)ですね。厳密には違うかもしれませんが、目的と効果の面で同一です。テストケースもモジュールを使用するユーザーの立場で見るべきであり、テストケースはモジュールの具体的なものに依存してはいけません。抽象化された責任をテストすべきで、モジュールをテストしてはいけません。こうすると、柔軟なテストケースになり、最終的にはどのようなモジュールでもテストすることができます。

それにもかかわらず、内部実装のテストが必要だと思われるモジュールがあれば、それは対象の内部実装が独立した責任を持つ別途モジュールとして抽出すべき、というシグナルである可能性が高いです。内部実装をクラスまたはモジュールで抽出し、そのモジュールを使用する構造に変更した場合、抽出されたモジュールのテストケースを作成し、外部インターフェースでテストすることができます。これは、内部実装が外部インターフェースに変更されたという良い事例に該当します。

テストケースはモジュールを使用するユーザーであると考えてみましょう。内部構造が全く分からないまま、公開されたことのみを知るユーザーのことです。それがテストケースとモジュールの関係で、テストケースもターゲットモジュールを依存関係に持つモジュールです。

有用なテストケース

有用なテストケースは、現在と将来の観点に分けて考えることができます。

現在において作成されるテストケースは、開発しているコードのテストを自動化します。そのため、入力値を調節したり決定値を確認するときに必要な時間を削減してくれます。その過程で内部実装に(あるいは外部インターフェースだと思われていた)該当するメソッドをテストすることもできます。単なる自動化という観点で役に立つからです。そして、テストケースが蓄積されるにつれ、以降に作成されたコードがサイドエフェクトによって古いコードを台無しにしてしまうことを防いでくれます。
TDDはアプリケーションの構造的な設計を直接サポートすることはありませんが、すでに組まれた協力構造の中で、各モジュールの役割と責任に必要なインターフェースを効果的に設計するのに役立ちます。そして、この過程で内部と外部が確実に区分され、精巧に整えられます。テストケースをあらかじめ作成しておくことは、モジュールを使用する立場から見て開発を始めるということだからです。

将来に向けたテストケースは、ターゲットモジュールの役割と責任を説明するだけでなく、具体的な使用方法まで提示する文書でなければなりません。そのため、ディスクリプションを読むだけでテストケースが何で、どうなることを期待しているかが分かるように作成する必要があります。ターゲットモジュールの機能が追加または変更されたとき、変更された内容が既存のスペックを満たしているかを自動的に確認し、変更されたコードによって発生する可能性のある問題を最小化しなければなりません。さらにターゲットモジュールが丸ごと変わったときでさえ、同じ機能を実行できなければなりません。

最終的に、TDDのサイクルを利用するしないに関係なく、テストケースを作成して消費した時間に対し十分な効率が得られるように、現時点のテストケースを作成しつつ、開発が進むにつれ将来を見通したテストケースに変更しなければなりません。開発を進行しながら不要なテストケースを消去したり、より良いテストに改善する必要があるでしょう。

プロジェクトにTDDやテストの自動化を導入した場合、モジュールと同様にテストケースも継続的に改善してリファクタリングしなければなりません。モジュールが外部インターフェースを維持したまま、内部コードをより速く、より簡潔に、より理解しやすいように改善できる理由は、テストケースが後ろで支えてくれているからです。モジュールと同様に最初から完璧なコードが完成することはなく、テストケースもより速く、簡潔に理解できるように改善しなければなりません。テストケースは、モジュールをテストするモジュールです。少なくともモジュールと同じ程度に重要視する必要があります。有用なテストケースを作成できるように、知恵を絞ってより良いテスト方法を考えなければなりません。

すべてのプロジェクト、すべての状況に当てはまる「有用なテストケースを作成する最高の方法」というものは存在せず、各プロジェクトの状況に応じて、それぞれ最善策は異なるでしょう。少なくとも、テストケースがどのように我々をサポートしてくれるか、あるいはどのようなテストケースが有用であるかを知ることは、各プロジェクトの状況に適したテストケースを作成する最小限の準備であり、スタート地点だと言えます。

内部実装は、公開されたインターフェースでのみテストする

すでに何度も言及していますが、内部実装をテストケースで直接行ってはいけません。公開されたインターフェースを通じてのみテストする必要があります。内部実装は、何らかの形で公開されたインターフェースによって使用します。そうでない場合は、当該コードは削除すべきコードと言えるでしょう。もちろん、内部実装を直接テストする方がより簡単にできますが、我々はテストケースを作成するためにテストケースを作成するのではなく、テストの自動化によって享受できるサポートを得るためにテストケースを作成するのです。

「内部実装をテストする必要があるか」論争において、内部実装を外部インターフェースに依存してテストすると、テストの完成度が把握しづらい、とよく言われることがあります。内部実装がどこまでテストされているか把握するのが困難であるからです。このようなときに役立つ指標がカバレッジ(coverage)です。

カバレッジは、公開されたインターフェースで内部構造をテストするときのように、テストケースだけではモジュールのテスト範囲を判断するのが難しいときに参照するテストの量的な品質を示す指標です。したがって質的な品質は保証されません。なぜなら、全く役に立たないテストケースを作成しても100%に近い結果を作ることができるからです。カバレッジはテストケースの質的な品質は保証しませんが、テストケースがモジュールのどの部分までテストしてあり、今後どこをテストするかが確認できる尺度を提供します。

まとめ

結論として、テストケースも開発するモジュールと同じように扱うべきであり、モジュールをテストする責任を持つモジュールと考えることができます。テストケースとモジュールとの関係がモジュールとモジュールとの関係だと考えるなら、テストケースがターゲットモジュールをどのように扱うべきかも明確になります。

フロントエンドの開発者にとって、テストの自動化はかなり難しい課題の1つであると思います。コードで作成するテストケースは、入力と出力がコードで表現可能なデータであるとき、また出力が明確な場合に適しています。結果がサイドエフェクトであったり、データで確認するのが難しいケースや、表示される領域をカバーする開発者にとっては難しい分野だと言えるでしょう。幸いなことに、フロントエンドの発展とともにフロントエンドをテストする分野もはるかに発展しており、最近はむしろどのような方法を選択するべきかで悩むことが多いと思います。

当然ながら、完全版のようなテストケースはなく、テストツールや方法論もソフトウェアプロジェクトの領域で継続して変化や改善が見られるでしょう。変化の中で本当に重要なことは、テストの効率と効用性という点を忘れてはなりません。

TOAST Meetup 編集部

TOASTの技術ナレッジやお得なイベント情報を発信していきます
pagetop