「ほとんどのユニットテストが役に立たない理由」を読んで

数ヶ月前、私はJames O Coplienのほとんどのユニットテストが役に立たない理由という記事に出会いました。Jamesはほとんどのユニットテストは無意味であると考えていて、タイトルは内容をそのまま正確に表しています。彼は追加記事で議論をさらに展開しています。私は彼の議論に大変興味をそそられました。というのは、私はユニットテストから多くの利益を得ているからです。私たちはどうしてこのような異なる見解を持つに至ったのでしょうか? 私が何かを見逃したのでしょうか? 結局のところ私は彼の見解に賛成できませんでした。以下は彼の記事に対する私の意見です。

ユニットテストが必要な場合

私の経験では、ユニットテストはアルゴリズムロジックに対して行う時に最も有益です。結合度の高いコードについてはその性質から特に有益ではありません。結合度が高いコードはユニットテストのために多くのモックオブジェクトを必要としますが、テストそのものがそれほど関心があるものではありません。この種のコードはユニットテストからは多くのメリットを得られませんが、それに代わって統合テストにおいてよりよいテストができます。コードのタイプの違いについてはSelective Unit Testing – Costs and Benefitsを参照してください。

アルゴリズムロジックの例は携帯電話システムで使われる仮番号プールです。プールにある電話番号はネットワークのルーティングコールのために使用されます。通常は仮番号を得るために要求が出され、それを数百ミリ秒使って、再び解放します。使用されている間、それはプールの中でビジーにマークされます。解放されるとフリーに変わります(別のコールへの配付が可能になります)。(ネットワークエラーなどのため)解放要求が受け付けられる保証はないので、プールにはタイムアウトの仕組みが必要です。ある番号が例えば10秒を越えてビジーにマークされていれば、解放要求が受け付けられなくてもタイムアウトになり、フリーにマークされます。このプールの機能はユニットテストに適しています。タイムアウトのような処理では時間の要素にダミーの値を置いてテストするのも1つの方法です。TDD, Unit Tests and the Passage of Timeを参照してください。

なぜユニットテストをするのか?

十分テストされた部品。アプリケーション全体をテストする場合に、十分テストされた部品を使うと好都合です。それは簡単な部品から機能を構成していく標準的なボトムアップ手法です。前述した例では、プールの機能のユニットテストが行われていれば、システム全体をテストする時に、残りのシステムに集中して動作確認をすることができます。もしユニットテストが行われていなければ、プールの動作に問題が発見されるかもしれませんが、コードの中で問題がどこにあるのか見つけるためには多くの労力が必要になるでしょう。どこかに問題が潜んでいる可能性はあるのですから。

デザインの分離。ユニットテストの対象となるシステムの構成部品をデザインする際、意識せずとも種々の部品を可能な限り細かく分離するはずです。そうしないと、ユニットテストはとても難しくなり、しばしば複雑な環境設定が必要となります。私自身、システムを設計中にユニットテストを書き始めてから、自分のコードがかなりうまく分離されるようになったことに驚きました。分離をさせないと、ユニットテストの修復が非常に困難になります。既存するシステムの部品は通常、複雑に絡み合ってしまっています。

素早いフィードバック。作業中のシステムの全機能をテストし終えるには、時間がかかることもあるでしょう。また、他の部品のテストを先に済ませる必要があることもあります。それ故に、段階ごとに統合テストを行うのは難しいのです。ユニットテストでは、間違いがあればすぐにフィードバックが得られます。例えば、私は、オフ・バイ・ワンエラーを起こさなくなりました。なぜなら、コードを書いたらすぐに境界値を確認するテストを行い、正常に作動するかをテストするようになったからです。

コンテキスト。テストで必要とするコンテキストをセットアップする場合、完全なシステムよりもユニットテストの中でセットアップした方が簡単な場合があります。前述のプールの例をとってみると、小さなプールを作成し、そのプールを要求でいっぱいにします。そしてプールにフリーの番号がない時に動作を確認する、という方が容易だということです。同じような状況をシステム全体に構築するのは困難です。仮番号が使用されている時間が短ければ、なおさらです。

議論の穴

私は、Jamesがユニットテストについて、いくつかの点で誤解をしている、または事実を曲げていると思っています。

一年経っても失敗しないテストの削除。Jamesは、一年経っても失敗しないユニットテストからは、何の情報も得られないので削除してよい、と主張しています。
しかし、ユニットテストの失敗は、コードの設計中にミスをしているということですから、コンパイルエラーの場合と同様に、すぐに修復を行うでしょう。ユニットテストに失敗しているのに、コードをチェックインすることはしません。ですから、ユニットテストが失敗したとしても、それは一時的なものなのです。

完全にテストすることは不可能。オリジナルそして追加記事の両方にて、Jamesはコードを完全にテストすることは不可能であると述べています。{プログラムカウンタ、システム状態}によって定義されている状態空間が巨大であることは確かですが、これは統合テストにおいても同じことが言えます。ですから、ユニットテストに対してだけの反対論にはなり得ません。

どの部品がどう使われるか分からない。Jamesは後の記事で紹介したマップの例のなかで、マップが5つ以上のアイテムを保有することはあり得ないと指摘していました。彼の指摘はもっともですが、統合テストの段階でもテストしていることに変わりはありません。本番環境では5つ以上のアイテムに遭遇する可能性もあります。どんな場合でも、より大きな値に備えておく方が賢明です。そのためのトレードオフは個人的に苦ではありません。最大使用サイズの大小にかかわらず、コストは低いですし、ロジックの多くも同じですから。

正しい振る舞いとは? Jamesの意見によると、ビジネス価値のあるテストとは、ビジネス要求に由来するテストだけです。ユニットテストは完成した機能ではなく構成部品を検証するテストに過ぎないため、信頼を得られません。ユニットテストは、この機能はこう動作すべきだというプログラマの思いつきに基づいています。しかしプログラマは、いつも要求を小さな部品にかみくだいて把握しているのであり、それがプログラミングの手順なのです。時には誤解もあるでしょうが、それは例外であり、いつもそうではないというのが私の見解です。

リファクタリングでテストが壊れる。コードをリファクタリングすると、テストが壊れることがあります。しかし、私の経験上、これは大した問題ではありません。例えば、メソッドのシグネチャが変われば、全部のテストを調べて呼び出しのあった箇所に追加のパラメータを加えなければなりませんが、この作業は大抵すぐに終わりますし、たびたび起こることでもありません。理論上は大変に思えますが、実際はそうでもないのです。

アサート。Jamesは、ユニットテストをアサートに置き換えるよう勧めています。アサートは役に立つ機能ですが、ユニットテストの代わりにはなりません。もしアサートが失敗すれば、本番システムにも不具合が残っているということです。アサートでもユニットテストでも検証できることなら、本番ではなくテストで問題を見つける方が効果的です。

まとめ

Jamesが考えるユニットテストの価値には賛同できないものの、彼の記事は楽しく読ませてもらいました。統合テストの重要性など、彼の見解にはうなずける部分もたくさんありました。また、同意できない部分について考えたおかげで、私自身の考えや、なぜそう思うのかという理由をより明確に示すことができたと思います。

ユニットテスト自体はゴールではありませんし、全ての状況で役に立つというわけでもありません。しかし、私は多くの場面でユニットテストを有効活用しています。私にとってユニットテストとは、極めて重要なソフトウェア開発手法の1つなのです。