単体テストの話
単体テストが難しすぎる件について
テストコードを書くにあたって
テスト対象となるコードには、そのコードを利用する側が期待する振る舞いがあるはずです。この期待する振る舞いをすることを保証するテストコードが良いテストコードと言えます。
従って、良いテストコードを書こうと思ったらまずその対象となるコードに期待される振る舞いを明確にするところから始めなければなりません。関数の振る舞いを記述する方法にはいろいろあるかと思いますが、僕はDesign by Contract(契約による設計)の理念に沿って関数の振る舞いを記述するのが良いのではないかと思います。
Design by Contract
Design by Contractとは、関数の仕様をを契約、すなわち事前条件・事後条件・不変条件によって記述するソフトウェア設計技法です。
詳しい説明は他に譲りますが、事前条件とはその関数を利用する側が守るべき義務、事後条件とは事前条件が満たされた場合に関数が果たす責務のことをいい、不変条件は関数が事後条件を達成するにあたって守らなければいけない制約を表します。
例外の扱いに関しては諸説ありますが、僕は事前条件が満たされたにもかかわらず、不変条件や事後条件を満たせない場合を例外とするのが良いのではないかと思います。(ここでは、事前条件が満たされなかった場合の関数の振る舞いは未定義であるとします。)
テストコードを書く際には、事前条件が満たされなかった場合に対するテストを実施してはならないものとします。事前条件が満たされなかった場合の動作をテストコードによって規定した場合、それは担保された(≒関数を利用する側が期待しても良い)関数の振る舞いの一部となるわけですから、契約として記述されるべきと考えます。
意味のあるテストコード
Design by Contractの考えに基づけば、契約が履行されることを保証するテストコードは意味のあるテストコードであると言えます。逆に、契約の履行を保証する役割を果たさないテストコードや契約と何ら関係ない動作を担保するテストコードは(この考えのもとでは)無意味であると言えます。
以降の議論はこの前提に基づいて行われるものとします。異なる前提を持ち出す場合は、ここから先に書いてあることを鵜呑みにしてはいけません。
テストコードを書くべきか
上述したような意味のあるテストコードなら書いたほうがいいです。近年では「テストを書く工数が・・・」みたいな話は様々に否定されてきていますので、意味のあるテストコードは書いたほうが良いということは自明であるとしましょう。
問題は、エンジニアなら誰でも良いテストコードがかけるわけではないということです。良いコードを書くのが難しいのと同様(あるいはそれ以上)に、良いテストコードを書くのは難しく、高いスキルが要求されます。一般的な開発組織においては、メンバー全員に良いテストコードをかくことを期待するのは結構難しいと思われます。
良いテストコードを書く確率が低いエンジニアに対して「テストを書いてくれ」というべきか、「(質の低いテストコードを書くくらいなら)テストを書かなくていい」というべきかは明確な答えが出せていません。
意味のない、信頼度の低いテストを書くことによって得られるのは偽りの安心です。テストコードがあるからなにか間違ったことをしたときはテストが通らなくなるだろう、という最も根源的な期待さえ抱くことは許されません。逆に、テストを書かなくてもよいとすれば、割れ窓理論により「テストは書かなくていいんだ」という認識が組織全体に広まってしまう問題があります。
どちらも治安は悪いのですが、いまのところ僕のとっているポリシーとしては「書かれたテストコードを否定しない。テストコードを書かなくても文句を言わない(※当然ですが、意味のあるテストを書けるエンジニアがサボるのは認めない)」ということにしています。たぶん間違っているので、ここに関して良いアイデアをお持ちの方はこっそり教えて下さい。
ホワイトボックステストの功罪
契約プログラミングに基づくテストコードの記述にあたり、頭の痛い問題がホワイトボックステストとどう向き合うかです。
原則として、ホワイトボックステストはテスト対象のコードの利用者からみた振る舞いの妥当性を担保しません(※ホワイトボックステストの結果から演繹的に担保されることはあるとおもいますが)。意味のあるテストを書こうと思ったら必然的にブラックボックステストを中心に書いてもらうことになります。
ところが、多くの若手エンジニアが教材などで最初に学ぶテストはホワイトボックステストであることがほとんどです[要出典]。カバレッジ、同値クラス分割、境界値テストなどはすべてホワイトボックステストの概念ですが、そうとは知らずに「テストを書くときはこういう技法をつかえばいいんだ!」というところからはいっているエンジニアが多いような気がします。そして、ブラックボックステストが求められているところでホワイトボックステストを書いて満足しているケースが(僕の生息範囲では)散見されます。
ホワイトボックステストがテスト対象となるコードの実装に制約をかけるテストであることを理解しているエンジニアは極めて稀な存在です。近年では、インタフェースを固定して実装を容易に切り替えられるようにする設計技法が高い評価を得ていますが、そのような設計のもとでホワイトボックステストを書いてしまうと実装の切り替えに強い抵抗を生み、結果として設計技法の良さが損なわれる(もちろんテストも直せばいいのですが、現実問題として「そこまでしてコードをいじるくらいなら既存踏襲でいいや」となる方向に力が働く)ので相性が悪いのではないでしょうか。
ブラックボックステストと比べると、ホワイトボックステストのほうが体系だっていて説明しやすいので仕方ないかなという諦めの思いが結構強いのですが、現代的なプログラミングではホワイトボックステストの使いどころには注意が必要だと感じています。
テストが難しいコード
ちゃんと調べたわけではありませんが、体感的には結合度が低く、凝集度が高いコードほど意味のあるテストコードを書くのが難しくなる傾向があると思います。
例えば、数式の計算を行う関数に対して適切なブラックボックステストを書くのは思っているよりも難しいです。どんな実装であるかに意味があるのであればホワイトボックステストでいいかもしれませんが、一般的には実装よりもその関数の性質が重要であり、特定の入力に対する出力の妥当性をテストすることは、しないよりはマシな可能性がありますが実効性はほとんどありません。
先述の通り、無意味なテストコードは偽りの安心を抱かせるだけなので、できれば書きたくありません。しばしば現れるテストが難しいコードに対しては、不適切な実装を咎めるテストコードが有効となる場合があります。すなわち、ある入力に対する出力値をそのままテストするのではなく、関数が期待する振る舞いをした際に出力値が満たすべき性質がちゃんと満たされているかどうかをテストするコードを書き、入力はテスト実行のたびに乱数などを用いてランダムに決定するというテクニックが(一応)あります。偽陰性となる可能性がありますが、CI環境が整備されていて十分に高い頻度でテストが実行されるならば、ないよりはマシという判断になるかもしれません。
テストコードかくあるべし
当たり前ですが、テストコードは万能の検証手段ではありません。テストコードができることには(いまのところ)限界があり、テストを書く工数も無限に確保できるわけではないので、エンジニアにはどこまで頑張るのが合理的なのかを高速に判断できることが求められます。
これを考えるにあたって、テストコードを書く際にはそのテストが何が担保できていて、何が担保できていないのかが明らかにすることが非常に重要であると思います。
あるテストコードが何を担保できるのかを説明することは(広く動作を担保できる良いテストを書くことに比べれば)難しくはないと思います。テストを書くときは、この点が明らかになるようにコメントやテストケース名などを工夫するように心がけています。
おわりに
長々とわかりにくい文章を書き連ねてきましたが、まとめると
です。
なんでもかんでもホワイトボックステストのコードを書いて十分なテストを書いたつもりになっているのが気になったので、テストの良し悪しについて議論をした際に考えたことをメモって置こうと思ったらむちゃくちゃ長くなってしまいました。ごめんなさい。