web-dev-qa-db-ja.com

TDDは防御的プログラミングを冗長にしますか?

今日、私は同僚と興味深い議論を交わしました。

私は防御的なプログラマーです。私は "クラスはそのクラスのオブジェクトがクラスの外から操作されたときに有効な状態であることを保証する必要があります"のルールは常に遵守されなければならないと信じています。このルールの理由は、クラスがそのユーザーが誰であるかを認識していないことと、クラスが不正な方法で操作された場合に予想どおりに失敗するはずであることです。私の意見では、そのルールはすべてのクラスに適用されます。

今日話し合った特定の状況で、コンストラクターへの引数が正しいことを検証するコードを作成しました(たとえば、整数パラメーターが0より大きい必要があります)。前提条件が満たされていない場合、例外がスローされます。一方、私の同僚は、ユニットテストでクラスの不正な使用を検出する必要があるため、このようなチェックは冗長であると考えています。さらに、防御的プログラミングの検証も単体テストする必要があるため、防御的プログラミングは多くの作業を追加するため、TDDには最適ではないと考えています。

TDDが防御的プログラミングを置き換えることができるというのは本当ですか?結果として、パラメーターの検証(およびユーザー入力を意味するわけではありません)は不要ですか?または、2つの手法は互いに補完し合うのでしょうか?

105
user2180613

それはばかげています。 TDDはコードに強制的にテストをパスさせ、すべてのコードに強制的にいくつかのテストを実行させます。これは、コンシューマーが誤ってコードを呼び出すことを防ぎません。また、プログラマーがテストケースを見落とすことを魔法のように防ぎません。

どの方法でも、ユーザーにコードを正しく使用させることができません。

ありますis TDDを完全に実行した場合、それを実装する前にテストケースで> 0チェックをキャッチし、これに対処する-おそらくチェックを追加して。しかし、TDDを実行した場合、要件(コンストラクターで> 0)はfirstが失敗するテストケースとして表示されます。したがって、チェックを追加した後にテストを提供します。

また、いくつかの防御条件をテストすることも妥当です(ロジックを追加したのに、なぜ簡単にテストできるものをテストしたくないのですか?)。なぜあなたがこれに同意しないように見えるのか、私にはわかりません。

または、2つの手法は互いに補完し合うのですか?

TDDがテストを開発します。パラメータの検証を実装すると、それらが渡されます。

196
enderland

防御プログラミングと単体テストは、エラーをキャッチする2つの異なる方法であり、それぞれに異なる長所があります。エラーを検出する唯一の方法を使用すると、エラー検出メカニズムが脆弱になります。両方を使用すると、公開APIでないコードでも、どちらか一方が見逃した可能性のあるエラーをキャッチできます。たとえば、誰かがパブリックAPIに渡された無効なデータのユニットテストを追加するのを忘れている可能性があります。適切な場所ですべてをチェックすることは、エラーをキャッチする機会が増えることを意味します。

情報セキュリティでは、これは多層防御と呼ばれます。複数の防御層があることで、1つが失敗した場合でも、それを捕捉する他の層が確実に存在します。

あなたの同僚は1つのことについて正しいです:あなたすべき検証をテストしますが、これは「不必要な作業」ではありません。これは他のコードをテストするのと同じです。無効なものも含めて、すべての使用法が期待どおりの結果になることを確認する必要があります。

32
Kevin Fee

TDDは防御的なプログラミングを完全に置き換えるものではありません。代わりに、TDDを使用して、すべての防御が適切に行われ、期待どおりに機能することを確認できます。

TDDでは、最初にテストを記述せずにコードを記述することは想定されていません。赤、緑、リファクタリングのサイクルに従ってください。つまり、検証を追加する場合は、最初にこの検証を必要とするテストを記述します。問題のメソッドを負の数とゼロで呼び出し、例外がスローされることを期待します。

また、「リファクタリング」ステップを忘れないでください。 TDDはtest-駆動型ですが、これはtest-onlyを意味するものではありません。それでも適切な設計を適用し、賢明なコードを書く必要があります。防御的なコードを書くことは、期待をより明確にし、コード全体をより堅牢にするので賢明なコードです。起こり得るエラーを早期に発見することで、デバッグが容易になります。

しかし、エラーを特定するためにテストを使用するべきではないでしょうか?アサーションとテストは補完的です。優れたテスト戦略では、ソフトウェアが堅牢であることを確認するためにさまざまなアプローチを組み合わせます。単体テストのみ、統合テストのみ、またはコード内のアサーションのみでは不十分であり、許容できる努力でソフトウェアを十分に信頼できる適切な組み合わせが必要です。

次に、あなたの同僚の非常に大きな概念的な誤解があります:単体テストはクラスのテストusesではなく、クラスitselfは単独で期待どおりに動作します。統合テストを使用して、さまざまなコンポーネント間の相互作用が機能することを確認しますが、考えられるテストケースの組み合わせの爆発により、すべてをテストすることは不可能です。したがって、統合テストでは、いくつかの重要なケースに限定する必要があります。 Edgeケースとエラーケースもカバーするより詳細なテストは、単体テストにより適しています。

30
amon

テストは、防御的なプログラミングをサポートおよび保証するためにあります

防御的プログラミングは、実行時にシステムの整合性を保護します。

テストは(ほとんど静的)診断ツールです。実行時には、テストが見えなくなります。それらは、高いレンガの壁や岩のドームを設置するために使用される足場のようなものです。建設中に足場で支えているため、構造体から重要な部品を取り残さないでください。あなたは、建設中にそれを支えて、促進するすべての重要な部分を入れる足場を持っています。

編集:アナロジー

コード内のコメントの類推についてはどうですか?

コメントには目的がありますが、冗長である場合や有害な場合もあります。たとえば、コードに関する本質的な知識をcommentsに入れてからコードを変更すると、コメントは無意味になり、最悪の場合は有害になります。

たとえば、MethodAはnullをとることができず、MethodBの引数は> 0。次に、コードが変更されます。現在、Aはnullで問題なく、Bは-10までの小さい値をとることができます。既存のテストは機能的に間違っていますが、引き続き合格します。

はい、コードを更新すると同時にテストを更新する必要があります。また、コードを更新すると同時にコメントを更新(または削除)する必要があります。しかし、私たちは皆、これらのことが常に起こるとは限らないこと、そしてその間違いが犯されることを知っています。

テストでは、システムの動作を確認します。その実際の振る舞いはシステム自体に固有ですnotテストに固有です。

何がうまくいかないのでしょうか?

テストに関する目標は、問題が発生する可能性のあるすべてを考え、適切な動作をチェックするテストを記述し、すべてのテストに合格するようにランタイムコードを作成することです。

つまり、防御的プログラミングがポイントです。

テストが包括的である場合、TDDdrives防御的プログラミング。

より多くのテスト、より防御的なプログラミングの推進

バグが必然的に見つかると、バグを明らかにする条件をモデル化するために、より多くのテストが作成されます。次に、コードが修正され、thoseテストに合格するコードが追加され、新しいテストはテストスイートに残ります。

良いテストのセットは、良い引数と悪い引数の両方を関数/メソッドに渡し、一貫した結果を期待します。これは、テストされたコンポーネントが前提条件チェック(防御プログラミング)を使用して、渡された引数を確認することを意味します。

一般的に言えば...

たとえば、特定のプロシージャへのnull引数が無効な場合、少なくとも1つのテストがnullを渡し、何らかの "無効なnull引数"例外/エラーが発生することが予想されます。

もちろん、他の少なくとも1つのテストでvalid引数を渡すか、または大きな配列をループして多数の有効な引数を渡して、結果の状態が適切であることを確認します。

テストがそのnull引数を渡さず期待どおりの例外でスラップされる場合(そして、渡された状態をコードが防御的にチェックしたため、その例外がスローされた場合)、nullクラスのプロパティに割り当てられたり、あるべきではないある種のコレクションに埋もれてしまう可能性があります。

これにより、ソフトウェアの出荷後に、クラスインスタンスが渡されるシステムの完全に異なる一部の場所で、離れた地理的ロケールで予期しない動作が発生する可能性があります。それは、私たちが実際に避けようとしていることですよね?

さらに悪くなる可能性もあります。無効な状態のクラスインスタンスは、後で使用するために再構成するときにのみ障害を発生させるために、シリアル化して保存できます。わかりません、多分それはそれ自身の永続的な構成状態を逆シリアル化できないため、シャットダウン後に再起動できないある種の機械制御システムである可能性があります。または、クラスインスタンスがシリアル化されて、他のエンティティによって作成されたまったく異なるシステムに渡され、thatシステムがクラッシュする可能性があります。

特に、他のシステムのプログラマーが防御的にコーディングしなかった場合

16
Craig

TDDの代わりに「ソフトウェアテスト」一般について、そして「防御的プログラミング」一般の代わりに、アサーションを使用する防御的プログラミングを実行する私のお気に入りの方法について話しましょう。


したがって、ソフトウェアテストを行うので、プロダクションコードへのassertステートメントの配置を中止する必要があります。これが間違っている方法を数えてみましょう:

  1. アサーションはオプションであるため、アサーションが気に入らない場合は、アサーションを無効にしてシステムを実行してください。

  2. アサーションは、テストができない(およびすべきでない)ことをチェックします。アサーションにはホワイトボックスビューがあるのに対し、テストにはシステムのブラックボックスビューがあるはずです。 (もちろん、彼らが住んでいるので。)

  3. アサーションは優れたドキュメントツールです。コメントは、同じことを主張するコードの一部ほど明確でした、または今後もそうです。また、ドキュメントはコードが進化するにつれて古くなる傾向があり、コンパイラーによって強制されることは決してありません。

  4. アサーションはテストコードのエラーをキャッチできます。テストが失敗し、誰が間違っているのか分からない状況に遭遇したことがありますか?

  5. アサーションはテストよりも適切な場合があります。テストでは、機能要件で規定されている内容をチェックしますが、コードは、それよりもはるかに技術的な特定の仮定を行わなければならないことがよくあります。機能要件文書を作成する人は、ゼロによる除算をほとんど考えません。

  6. アサーションは、テストが広く示唆するエラーを特定します。したがって、テストはいくつかの広範な前提条件を設定し、いくつかの長いコードを呼び出し、結果を収集し、それらが期待どおりでないことを発見します。十分なトラブルシューティングがあれば、最終的には問題のある場所を正確に見つけることができますが、アサーションは通常、最初にそれを見つけます。

  7. アサーションはプログラムの複雑さを軽減します。作成するコードの1行ごとにプログラムが複雑になります。アサーションとfinalreadonly)キーワードは、実際にプログラムの複雑さを軽減する唯一の2つの構成要素です。それは貴重です。

  8. アサーションは、コンパイラがコードをよりよく理解するのに役立ちます。自宅でこれを試してください:void foo( Object x ) { assert x != null; if( x == null ) { } }コンパイラは、条件x == nullは常にfalseです。これは非常に便利です。

上記は私のブログからの投稿の要約でした 2014-09-21 "Assertions and Testing"

9
Mike Nakis

ほとんどの回答には重要な違いが欠けていると思います。コードの使用方法によって異なります。

問題のモジュールは、テストしているアプリケーションに関係なく、他のクライアントによって使用されますか?サードパーティが使用するライブラリまたはAPIを提供している場合、有効な入力でのみコードを呼び出すようにする方法はありません。すべての入力を検証する必要があります。

しかし、問題のモジュールがユーザーが制御するコードでのみ使用されている場合は、友達にポイントがある可能性があります。単体テストを使用して、問題のモジュールが有効な入力でのみ呼び出されることを確認できます。事前条件チェックは良い習慣と見なすことができますが、それはトレードオフです。私はあなたがknowが発生することのない条件をチェックするコードを散らかします、それは単にコードの意図を覆い隠すだけです。

前提条件チェックにさらにユニットテストが必要になることには同意しません。一部の形式の無効な入力をテストする必要がないと判断した場合、関数に前提条件チェックが含まれているかどうかは問題になりません。テストでは、実装の詳細ではなく、動作を検証する必要があることに注意してください。

5
JacquesB

TDDの練習を始めたとき、「<無効な入力>のとき、オブジェクトは<特定の方法>に反応する」という単位テストのユニットテストが2〜3回増加したので、この議論は私を困惑させます。あなたの同僚が、彼の機能が検証を行わずに、これらの種類の単体テストにうまく合格していることに疑問を感じています。

逆の場合、単体テストは、他の関数の引数に渡される悪いoutputsを生成していないことを示しているため、証明するのがはるかに困難です。最初のケースと同様に、Edgeケースの完全なカバレッジに大きく依存しますが、すべての関数入力が、ユニット入力をテストした他の関数の出力からのものであり、ユーザー入力やサードパーティのモジュール。

言い換えると、TDDが行うことは必要検証コードを回避することではなく、忘れるを回避するのに役立ちます。

3
Karl Bielefeldt

私はあなたの同僚の発言を他のほとんどの回答とは異なって解釈すると思います。

議論は次のように思えます:

  • すべてのコードは単体テストされています。
  • コンポーネントを使用するすべてのコードは私たちのコードであるか、そうでない場合は他の誰かによってユニットテストされます(明示的には述べられていませんが、「ユニットテストはクラスの不正な使用をキャッチする必要があります」から理解できます)。
  • したがって、関数の呼び出し元ごとに、コンポーネントをモックするユニットテストがどこかにあり、呼び出し元がそのモックに無効な値を渡した場合、テストは失敗します。
  • したがって、無効な値が渡されたときに関数が何をするかは問題ではありません。これは、テストでは発生しないことを示しているためです。

私にとって、この議論にはある程度の論理がありますが、考えられるすべての状況をカバーするには、ユニットテストに依存しすぎています。単純な事実は、100%のライン/ブランチ/パスのカバレッジは、必ずしも発信者が通過する可能性があるすべてのvalueを実行するわけではないのに対し、発信者のすべての可能な状態の100%のカバレッジ(つまり、 、その入力と変数のすべての可能な値)は計算上実行不可能です。

したがって、呼び出し側をユニットテストして、(テストに関する限り)不正な値が渡されないことを確認する傾向があり、追加は、コンポーネントが認識可能な方法で失敗することを要求します不正な値が渡された(少なくとも、選択した言語で不正な値を認識できる限り)。これは、統合テストで問題が発生した場合のデバッグを支援し、同様に、その依存関係からコードユニットを分離するのが厳密でないクラスのユーザーを支援します。

ただし、値<= 0が渡されたときの関数の動作を文書化してテストすると、負の値は無効ではなくなります(少なくとも、どの値よりも無効になることはありません) throwへの引数です。例外もスローされるようにドキュメント化されているためです!)。呼び出し元は、その防御行動に依存する権利があります。言語が許せば、これはいずれにせよ最良のシナリオである可能性があります-関数has no "無効な入力"ですが、関数を呼び出して例外をスローしないように期待している呼び出し元はunit-それらがそれを引き起こす値を渡さないことを保証するために十分にテストされました。

あなたの同僚はほとんどの回答よりも完全に間違っているとは思っていませんが、私は同じ結論に達します。つまり、2つの手法は互いに補完し合うということです。防御的にプログラムし、防御チェックを文書化し、テストします。コードのユーザーが間違いを犯したときに有用なエラーメッセージを利用できない場合、作業は「不要」になります。理論的には、コードを統合する前にすべてのコードを完全にユニットテストし、テストでエラーが発生しない場合、エラーメッセージは表示されません。実際には、TDDや完全な依存性注入を行っている場合でも、開発中に調査を行ったり、テストが失敗したりする可能性があります。その結果、コードが完全になる前にコードが呼び出されます。

2
Steve Jessop

テストの適切なセットは、クラスのexternalインターフェースを実行し、そのような誤用が正しい応答(例外、または「正しい」と定義したもの)を生成することを確認します。実際、私がクラスに対して書く最初のテストケースは、範囲外の引数でそのコンストラクタを呼び出すことです。

完全にユニットテストされたアプローチによって排除される傾向がある防御的プログラミングの種類は、外部コードに違反できないinternal不変条件の不必要な検証です。

私が時々使う便利なアイデアは、オブジェクトの不変条件をテストするメソッドを提供することです。分解メソッドはそれを呼び出して、オブジェクトに対する外部アクションが不変条件を壊さないことを検証できます。

1
Toby Speight

テストはクラスのコントラクトを定義します。

結果として、テストの不在definesndefined behaviour を含む契約。そのため、nullFoo::Frobnicate(Widget widget)に渡し、実行時に大混乱が発生しても、クラスの契約内にとどまります。

後で、「未定義の動作の可能性を望まない」と決定します。これは賢明な選択です。つまり、nullFoo::Frobnicate(Widget widget)に渡すための予期された動作が必要です。

そしてあなたはその決定を文書化して

[Test]
void Foo_FrobnicatesANullWidget_ThrowsInvalidArgument() 
{
    Given(Foo foo);
    When(foo.Frobnicate(null));
    Then(Expect_Exception(InvalidArgument));
}
1
Caleth

誤用に対する防御は機能であり、その要件のために開発されました。 (すべてのインターフェースが誤用に対する厳密なチェックを必要とするわけではありません。たとえば、非常に狭く使用されている内部インターフェースなど)

この機能にはテストが必要です。誤用に対する防御は実際に機能しますか?この機能をテストする目的は、機能が機能しないことを示すことです。つまり、チェックでキャッチされないモジュールの誤用を考案することです。

特定のチェックが必要な機能である場合、一部のテストの存在によりそれらが不要になると断言することは実際には無意味です。 (たとえば)パラメーター3が負の場合に例外をスローするのが関数の機能である場合、交渉はできません。それはそれをしなければならない。

しかし、私はあなたの同僚が実際に入力に対してspecificチェックの要件がない状況の観点から、意味のある入力に対応しており、悪い入力に対する特定の応答があると考えています:状況堅牢性について理解されている一般的な要件のみです。

一部のトップレベル関数へのエントリのチェックは、一部では、弱いまたは不適切にテストされた内部コードを予期しないパラメーターの組み合わせから保護するために行われます(コードが十分にテストされている場合、チェックは不要です。コードは単に "天気」という悪いパラメータ)。

同僚のアイデアには真実があり、彼がおそらく意味することはこれです:防御的にコーディングされ、すべての誤用に対して個別にテストされる非常に堅牢な低レベルの部分から関数を構築する場合、より高いレベルの関数が独自の大規模なセルフチェックなしで堅牢。

コントラクトに違反している場合、おそらく例外などをスローすることにより、下位レベルの関数の誤用につながります。

それに関する唯一の問題は、低レベルの例外が高レベルのインターフェイスに固有ではないことです。それが問題であるかどうかは、要件が何であるかに依存します。要件が単に「関数は誤用に対してロバストであり、クラッシュではなく何らかの例外をスローするか、ガベージデータで計算を継続する」である場合、実際には、それが存在する下位レベルの部分のすべてのロバストネスによってカバーされる可能性があります。構築されました。

関数がそのパラメーターに関連する非常に具体的で詳細なエラー報告の要件を持っている場合、低レベルのチェックはそれらの要件を完全には満たしません。それらは、関数が何らかの方法で爆発することのみを保証します(パラメーターの不適切な組み合わせで続行されず、不要な結果が生成されます)。特定のエラーを明確にキャッチして処理するようにクライアントコードが記述されている場合、正しく動作しない可能性があります。クライアントコード自体が入力として、パラメーターの基になるデータを取得している可能性があります。また、関数がこれらをチェックし、ドキュメントに記載されているように不正な値を特定のエラーに変換することを期待している可能性があります(これらを処理できるようにするため)エラーは適切に処理されません)処理されない他のエラーではなく、ソフトウェアイメージを停止する可能性があります。

TL; DR:あなたの同僚はおそらくばかではありません。要件は完全に明確にされておらず、「書かれていない要件」とは何かについてそれぞれが異なる考えを持っているため、同じことに関して異なる視点でお互いを越えて話しているだけです。パラメータチェックに特定の要件がない場合は、とにかく詳細なチェックをコーディングする必要があると考えています。同僚が考えているのは、パラメータが間違っている場合に、ロバストな下位レベルのコードを爆発させるだけです。コードを通じて書かれていない要件について議論することは多少非生産的です。コードではなく要件について同意しないことを認識してください。コーディングの方法は、要件が何であるかを反映しています。同僚のやり方は、要件に対する彼の見方を表しています。このように見ると、コード自体に正しい点や間違っている点がないことが明らかです。コードは、仕様がどうあるべきかについてのあなたの意見の単なる代理です。

1
Kaz

パブリックインターフェイスは誤用される可能性があり、誤用されます

同僚の「ユニットテストはクラスの不正な使用をすべてキャッチする必要がある」という主張は、プライベートではないすべてのインターフェースに対して厳密に偽です。パブリック関数が整数引数で呼び出すことができる場合、それはany整数引数で呼び出すことができ、呼び出されます。コードは適切に動作するはずです。パブリック関数の署名が受け入れる場合Java Doubleタイプの場合、null、NaN、MAX_VALUE、-Infはすべて可能な値です。ユニットテストでは、このクラスを使用するコードをテストできないため、クラスの不正な使用をキャッチできません。そのコードはまだ書かれておらず、あなたが書いていない可能性があり、間違いなくyour単体テストの範囲外であるためです。

一方、このアプローチは、(できればはるかに多くの)プライベートプロパティに対して有効である可能性があります-クラスが可能な場合ensureいくつかの事実は常にtrueです(たとえば、プロパティXはnullにできない、整数位置)最大長を超えていない場合、関数Aが呼び出されると、すべての前提データ構造が適切に形成されます)、パフォーマンス上の理由からこれを何度も確認することを避け、代わりに単体テストに依存するのが適切です。

1
Peteris

TDDのテストはコードの開発の間に間違いを見つけます。

ディフェンシブプログラミングの一部として記述した境界チェックは、コードの使用の間に間違いを見つけます。

2つのドメインが同じである場合、つまり、作成しているコードがこの特定のプロジェクトによって内部的にのみ使用される場合、TDDが、記述した防御的プログラミング境界チェックの必要性を排除することは事実ですが、それは- これらのタイプの境界チェックはTDDテストで具体的に実行されます


具体的な例として、財務コードのライブラリがTDDを使用して開発されたとします。テストの1つは、特定の値が負になることは決してないと主張する場合があります。これにより、ライブラリの開発者が、機能を実装するときにクラスを誤って誤用しないようにします。

しかし、ライブラリがリリースされて自分のプログラムで使用している場合、それらのTDDテストでは、負の値を割り当てることができます(公開されている場合)。境界チェックはそうします。

私の要点は、コードが(TDDの下で)より大きなアプリケーションの開発の一部として内部でのみ使用される場合、TDDアサートが負の値の問題に対処できる一方で、他のプログラマーが使用するライブラリになる場合TDDフレームワークとテストなし、境界チェックの問題。

0
Blackhawk

TDDとディフェンシブプログラミングは密接に関係しています。両方を使用することは冗長ではありませんが、実際には補完的です。関数がある場合は、その関数が説明どおりに機能することを確認し、そのテストを記述します。入力が正しくない、戻りが悪い、状態が悪いなどの場合に何が起こるかをカバーしないと、テストを十分に堅牢に記述できなくなり、すべてのテストが成功してもコードは壊れやすくなります。

組み込みエンジニアとして、関数を記述して2バイトを単純に加算し、次のような結果を返す例を使用します。

_uint8_t AddTwoBytes(uint8_t a, uint8_t b, uint8_t *sum); 
_

これで、単に*(sum) = a + bを実行しただけで機能しますが、some入力でのみ機能します。 _a = 1_および_b = 2_は_sum = 3_になります。ただし、合計のサイズがバイトであるため、オーバーフローにより_a = 100_および_b = 200_は_sum = 44_になります。 Cでは、この場合エラーを返し、関数が失敗したことを示します。例外をスローすることは、コードでも同じです。失敗を考慮しないか、それらを処理する方法をテストすることは長期的には機能しません。これらの条件が発生した場合、それらは処理されず、多くの問題を引き起こす可能性があるためです。

0
Dom