はじめに
昨今、テストピラミッドなどの側面からユニットテストの重要性が説かれていますが、クラス間が密に結合している等で適切なユニットテストを書くのが難しいという状況に陥ることは多いのではないでしょうか。そのような状況は、ユニットテストの解像度が低いために生まれると自分は考えます。
本記事では、防御的プログラミングと契約プログラミングという二種類のプログラミングの方法論を元にユニットテストを再考し、ユニットテストの解像度を高めることを目標とします。また、ユニットテストのより良い書き方を模索している人に本記事を読んでいただきたいです。
防御的プログラミングと契約プログラミングとは
防御的プログラミングと契約プログラミングとは次のようなプログラミングの方法論のことを指します。この二つの方法論については、こちらの記事にわかりやすくまとめられているので、ぜひ参考にしてください。
- 防御的プログラミング
- 与えられるデータを信用せずに防御的にプログラミングする方法
- 契約プログラミング
- 与えられるデータや返却するデータを厳密に定義する契約のもとでプログラミングする方法
つまり、"a", "b", "c"という文字列を受け取って文字列を返却するようなメソッドをあるクラスが持つとき、与えられるデータに着目すると、次のような実装の違いが生まれます1,2。2つのコードの差分はstringと"a"|"b"|"c"の部分で、後者の方が厳密に与えられるデータを定義しています。
class Sample {
say (param: string) {
if (!["a", "b", "c"].includes(param)) {
return null;
}
return "d";
}
}
class Sample {
say (param: "a"|"b"|"c") {
return "d";
}
}
防御的プログラミングと契約プログラミングの違い
防御的プログラミングと契約プログラミングの大きな違いとしては、契約プログラミングでは厳密な定義が要求されるということです。つまり、アプリケーション側で制御できない部分には契約プログラミングを適用することができず、この場合は防御的プログラミングでなければなりません。ただ、防御的プログラミングは契約プログラミングと比べると冗長なコードになりがちであるため、できるだけ契約プログラミングを採用したいところです3。
この違いを踏まえると、次のような観点で実装するのが良いのではと自分は考えます。
- 防御的プログラミング
- アプリケーション側で制御できない限定的な部分で利用
- 例:リクエストの受け取りや外部APIとの接続部分など
- 契約プログラミング
- アプリケーション側で制御できる部分で利用
- 例:ビジネスロジックの処理や画面の生成など
したがって、適当な階層構造のアーキテクチャを想定した時、次のように実装することができます1。
防御的プログラミングと契約プログラミングのユニットテスト
ユニットテストの実装という観点では、テストの目的が異なります。つまり、防御的なプログラミングは正しく防御できているか、契約プログラミングでは正しく契約ができているか、という部分が目的になります。この目的をわかりやすくすると、次のようになります。
- 防御的プログラミング
- 入力を適切に制御できているかを確かめるテスト4
- セキュリティ的な観点で重要
- 契約プログラミング
- 振る舞いが契約に従うかを確かめるテスト
- 実装者の観点で重要
したがって、適当な階層構造のアーキテクチャを想定した時、次のようにテストを実装する必要があります1。
最後に
本記事では、防御的プログラミングと契約プログラミングによりユニットテストを再考しました。ユニットテストを様々な観点から考察するためのきっかけとなれば幸いです。
また、本来はテストコードについても紹介するべきでしたが、筆者の時間的な都合により割愛しました。時間のある際にこの記事の考えをもとにしたテストコードを別の記事にて書く予定です。
また、リファクタリングにおいて重要なユニットテストを新たな観点から再考した記事であるため、下記アドベントカレンダーに投稿させていただきました。