それは私が何年もの間自分自身に問いかけてきた質問ですが、それでも満足のいく答えは得られていません。
しかし、引数の検証に関しては、次の2つのケースを区別する必要があると思います。
論理プログラミングエラーをキャッチするために引数を検証していますか?
if (foo == null) throw new ArgumentNullException("foo");
その一例である可能性が非常に高いです。
引数が無効である可能性があり、拒否する必要がある外部入力(ユーザーによって提供されるか、構成ファイルまたはデータベースから読み取られる)であるため、引数を検証していますか?
if (customerDateOfBirth == new DateTime(1900, 1, 1)) throw …;
このタイプの引数チェックの可能性があります。
(チーム外の誰かが使用するAPIを公開している場合は、ポイント2も大まかに当てはまります。)
単体テスト、契約による設計、およびある程度「早期に失敗する」などの方法論は、主に最初のタイプの引数検証に焦点を合わせていると思います。つまり、無効な入力ではなく、論理プログラミングエラーを検出しようとします。
その場合は、どのエラー検出方法を使用するかは実際には問題ではないと思います。それぞれに長所と短所があります。†極端な場合(たとえば、バグのないコードを作成する能力に絶対的な信頼がある場合)、これらのチェックを完全に削除することもできます。
ただし、コード内の論理エラーを検出するために選択する方法が何であれ、ユーザー入力などを検証する必要があるため、2種類の引数チェックを区別する必要があります。
†)契約による設計、単体テスト、および「早期失敗」の相対的な長所と短所を比較するアマチュアの不完全な試み:
(あなたはそれを求めていませんでしたが...私はいくつかの重要な違いに言及します。)
早期に失敗する(例:メソッドの開始時に明示的な引数の検証):
- ガードなどの基本的なチェック
null
を書くのは簡単です
- 同じ構文での論理エラーと外部入力の検証に対するガードを混同する可能性があります
- メソッドの相互作用をテストすることはできません
- メソッドのコントラクトを厳密に定義する(したがって考える)ことを推奨しません
ユニットテスト:
- 実際のアプリケーションを実行せずにコードを分離してテストできるため、バグの検出が迅速になります
- 論理エラーが発生した場合、各単体テストはコードの特定の「ユースケース」を表すため、原因を見つけるためにスタックをトレースする必要はありません。
- 複数のオブジェクト間の相互作用など、単一のメソッド以上のものをテストできます(スタブとモックを考えてください)
- 簡単なテスト(ガードなど
null
)を作成することは、「早期に失敗する」アプローチよりも手間がかかります(Arrange-Act-Assertパターンに厳密に準拠している場合)。
契約による設計:
- クラスのコントラクトを明示的に指定するように強制します(ただし、これは単体テストでも可能です—まったく別の方法で)
- クラスの不変条件(常に真でなければならない内部条件)を簡単に述べることができます
- 他のアプローチほど多くのプログラミング言語/フレームワークでサポートされていません