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


104

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

私は防御的なプログラマーです。「クラスは、クラスの外部から対話するときにオブジェクトが有効な状態になることを保証する必要があるというルールを常に順守する必要があると思います。このルールの理由は、クラスがそのユーザーが誰であるかを知らず、違法な方法で対話された場合に予想通り失敗する必要があるためです。私の意見では、この規則はすべてのクラスに適用されます。

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

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


120
コンストラクターチェックなしで完全に単体テストされたライブラリを使用するクライアントに渡すと、クラスコントラクトに違反します。これらの単体テストは今、どのような利点がありますか?
ロバートハーベイ

42
IMOそれは逆です。防御的なプログラミング、適切な前提条件と条件、およびリッチタイプシステムにより、テストが冗長になります。
ガーデンヘッド

37
「良い悲しみ」とだけ言う答えを投稿できますか?防御的プログラミングは、実行時にシステムを保護します。テストは、コンストラクターや他のメソッドに渡された無効な引数など、テスターが考えられるすべての潜在的なランタイム条件をチェックします。テストは、完了した場合、スローされる適切な例外を含む、ランタイムの動作が期待どおりであることを確認します無効な引数が渡されたときに行われる他の意図的な動作。しかし、テストでは、実行時にシステムを保護するのに苦労することはありません。
クレイグ

16
「単体テストはクラスの不正な使用をキャッチする必要があります」-ええと、どうやって?単体テストでは、正しい引数が指定された場合の動作と、誤った引数が指定された場合の動作が示されます。彼らはあなたにそれが与えられるすべての議論を示すことはできません。
OJFord

34
ソフトウェア開発についての独断的な考え方が有害な結論につながる可能性があるという良い例を見たことがないと思います。
-sdenham

回答:


196

それはばかげている。TDDは、コードにテストの合格を強制し、すべてのコードにいくつかのテストを強制します。消費者が誤ってコードを呼び出すことを防ぐことも、プログラマーがテストケースを見逃すことを魔法のように防ぐこともありません。

ユーザーにコードを正しく使用させる方法はありません。

そこはおそらくあなたがチェックを追加することで-あなたは完全にTDDをした場合は、それを実装する前にテストケースでは、あなたの> 0小切手を、キャッチし、これを対処しているだろうと判断されるわずかな引数は。ただし、TDDを実行した場合、要件(コンストラクターで0 より大きい)は最初に失敗したテストケースとして表示されます。したがって、チェックを追加した後にテストを提供します。

防御条件の一部をテストすることも合理的です(ロジックを追加したのに、なぜ簡単にテストできるものをテストしないのですか?)。なぜあなたはこれに反対するように見えるのか分かりません。

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

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


7
前提条件の検証をテストする必要があるという考えには同意しませんが、前提条件の検証をテストする必要性によって引き起こされる余分な作業は、最初に前提条件の検証を作成しないという議論であるという同僚の意見には同意しません場所。明確にするために投稿を編集しました。
user2180613

20
@ user2180613前提条件の失敗が適切に処理されることをテストするテストを作成します。チェックの追加は「余分な」作業ではなく、テストをグリーンにするためにTDDが必要とする作業です。あなたの同僚の意見はあなたがテストをしなければならないということであれば、それは失敗し、観察した後だけにして、その後、彼はTDD-純粋主義者の観点から、ポイントを持っているかもしれませんが、前提条件のチェックを実装します。彼がチェックを完全に無視するように言っているなら、彼はばかげている。TDDには、潜在的な障害モードのテストを書くのに先手を打てないということは何もありません。
RM

4
@RM前提条件チェックをテストするためのテストを書いているわけではありません。呼び出されたコードの予想される正しい動作をテストするためのテストを書いています。前提条件チェックは、テストの観点から、正しい動作を保証する不透明な実装の詳細です。呼び出されたコードで適切な状態を確保するためのより良い方法を考えている場合は、従来の前提条件チェックを使用する代わりに、そのようにしてください。このテストは、あなたが成功したかどうかを裏付けますが、それがどのように行われたはまだわかりません。
クレイグ

@ user2180613それは素晴らしい正当化です:Dソフトウェアの作成における目標がオーサリングと実行に必要なテストの数を減らすことである場合、ソフトウェアを作成しないでください-テストなし!
ガスドール

3
この答えの最後の文はそれを否定しています。
ロバートグラント

32

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

情報セキュリティでは、これは多層防御と呼ばれます。複数の防御層を備えているため、1つが失敗しても、他の人がそれをキャッチできます。

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


パラメーター検証は前提条件検証の形式であり、単体テストは事後条件検証であると言うのは正しいですか?
user2180613

1
「他のコードをテストするのと同じです。無効なものも含め、すべての使用法で期待される結果が得られるようにする必要があります。」この。渡された入力が処理するように設計されていない場合、コードはただ通過するべきではありません。これは「フェイルファースト」の原則に違反し、デバッグを悪夢のようにする可能性があります。
jpmc26

@ user2180613-実際にはそうではありませんが、開発者が期待する障害条件を単体テストでチェックし、ディフェンシブプログラミングテクニックでは開発者が期待しない条件をチェックします。単体テスト使用して、前提条件を検証できます(前提条件をチェックする呼び出し元に挿入されたモックオブジェクトを使用して)。
ペリアタブレアッタ

1
@ jpmc26はい、失敗テストの「期待される結果」です。いくつかの未定義の(予期しない)動作を静かに示すのではなく、失敗することを示すためにテストします。
-KRyan

6
TDDは自分のコードのエラーをキャッチし、防御的プログラミングは他の人のコードのエラーをキャッチします。したがって、TDDは、十分な防御力を確保するのに役立ちます。)
16

30

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

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

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

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

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


16

防御的なプログラミングをサポートおよび保証するためのテスト

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

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

編集:アナロジー

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

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

MethodAはnullを取得できず、MethodBの引数はである必要があるなど、コードベースの多くの固有の知識をテストに入力するとします> 0。その後、コードが変更されます。ヌルはAで大丈夫であり、Bは-10までの値を取ることができます。既存のテストは機能的に間違っていますが、引き続き合格します。

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

テストでは、システムの動作を検証します。その実際の動作はシステム自体に固有のものであり、テストに固有のものではありません

何が間違っている可能性がありますか?

テストに関する目標は、失敗する可能性のあるすべてを考え出し、正しい動作をチェックするテストを作成してから、すべてのテストに合格するようにランタイムコードを作成することです。

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

テストが包括的な場合、TDDは防御的なプログラミングを推進します

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

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

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

一般的に言えば...

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

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

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

これにより、ソフトウェアが出荷された後の地理的に離れた場所で、クラスインスタンスが渡されるシステムのまったく異なる部分で予期しない動作が発生する場合があります。そして、それは私たちが実際に回避しようとしている種類のものですよね?

さらに悪化する可能性もあります。無効な状態のクラスインスタンスは、後で使用するために再構成された場合にのみ障害を引き起こすために、シリアル化および保存できます。Geez、私は知らない、多分それはシャットダウンの後にそれ自身の永続的な設定状態をデシリアライズできないので再起動できないある種の機械的制御システムだろう。または、クラスインスタンスをシリアル化し、他のエンティティによって作成されたまったく異なるシステムに渡すと、そのシステムがクラッシュする可能性があります。

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


2
おかしなことに、ダウン票は非常に速く来たので、ダウン票者はおそらく最初の段落を超えて読むことができたはずです。
クレイグ

1
私はちょうどアップ投票うまくいけば、最初の段落を超えて読まずに...それを相殺すること:-)
SusanW

1
私は:-)(実は、私は何ができる少なくとも思えなかった。! -特にこのようなトピックに念のために残りの部分を読んでずさんであってはならない)
SusanW

1
私はおそらく持っていたと思った。:)
クレイグ

コードコントラクトなどのツールを使用して、コンパイル時に防御チェックを実行できます。
マシューホワイト

9

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


ソフトウェアのテストを行うので、実稼働コードにアサートステートメントを配置するのをやめるべきですよね?これが間違っている方法を数えてみましょう。

  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「アサーションとテスト」からの投稿の要約でした


私はこの答えにほとんど同意しないと思います。(5)TDDでは、テストスイートは仕様です。テストをパスする最も簡単なコードを記述する必要があります。(4)赤と緑のワークフローにより、テストが必要なときに失敗し、目的の機能が存在するときに合格することが保証されます。アサーションはここではあまり役に立ちません。(3,7)ドキュメントはドキュメントであり、アサーションはドキュメントではありません。しかし、仮定を明示的にすることにより、コードはより自己文書化されます。私はそれらを実行可能なコメントと考えています。(2)ホワイトボックステストは、有効なテスト戦略の一部となります。
アモン

5
「TDDでは、テストスイートが仕様です。テストをパスする最も単純なコードを記述する必要があります。」:これが常に良いアイデアだとは思いません。答えで指摘したように、検証する可能性のあるコード内の追加の内部仮定。互いにキャンセルする内部バグはどうですか?テストは成功しますが、コード内のいくつかの仮定が間違っているため、後に潜行性のバグが発生する可能性があります。
ジョルジオ

5

ほとんどの答えには重大な区別が欠けていると思います。それは、コードがどのように使用されるかによって異なります。

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

しかし、問題のモジュールがあなたが制御するコードによってのみ使用される場合、あなたの友人はポイントを持っているかもしれません。単体テストを使用して、問題のモジュールが有効な入力でのみ呼び出されることを確認できます。前提条件のチェックはまだ良い練習と考えられるが、それはトレードオフです:あなたが条件をチェックするコードI君ごみ知っているが発生したことがないことができ、それだけで、コードの意図をあいまいにしています。

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


4
呼び出されたプロシージャが入力の妥当性を検証しない場合(元の議論)、ユニットテストでは、問題のモジュールが有効な入力でのみ呼び出されることを確認できません。特に、無効な入力で呼び出される可能性がありますが、テストされたケースでは正しい結果を返すだけです-さまざまなタイプの未定義の動作、オーバーフロー処理などがあり、最適化が無効になっているテスト環境で期待される結果を返す可能性がありますが、本番で失敗します。
ペティス

@Peteris:Cのような未定義の動作を考えていますか?異なる環境で異なる結果をもたらす未定義の動作を呼び出すことは明らかにバグですが、前提条件チェックでも防止できません。例えば、ポインター引数が有効なメモリーを指していることをどのように確認しますか?
ジャックB

3
これは、最小のショップでのみ機能します。チームが6人を超えると、とにかく検証チェックが必要になります。
ロバートハーヴェイ

1
@RobertHarvey:その場合、システムは明確に定義されたインターフェースを備えたサブシステムに分割され、インターフェースで入力検証が実行される必要があります。
ジャックB

この。コードによって異なりますが、このコードはチームが使用するものですか?チームはソースコードにアクセスできますか?純粋に内部コードで引数をチェックするだけでは負担になる場合があります。たとえば、0をチェックして例外をスローし、呼び出し元がコードを調べて、このクラスが例外などをスローして待機できるようにします。オブジェクトは2レベル前にフィルターで除外されるため、0を受け取ることはありません。それがサードパーティによって使用されるライブラリコードである場合、それは別の話です。全世界で使用されるすべてのコードが書かれているわけではありません。
アレクサンダーフラー

3

TDDの練習を開始したとき、「<invalid input> when object is <certain way>」という形式のユニットテストが2倍または3倍に増加したため、この議論は私を困惑させます。あなたの同僚は、彼の機能が検証を行わずに、これらの種類の単体テストに合格することに成功しているのだろうかと思います。

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

言い換えれば、TDDが行うことは、それを忘れないようにするのに役立つほど、検証コードを必要とすることを妨げるものではありません。


2

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

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

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

私にとって、この議論にはいくつかの論理がありますが、あらゆる可能性のある状況をカバーするには単体テストに頼りすぎています。単純な事実は、100%の回線/分岐/パスカバレッジは、発信者が渡す可能性のあるすべてのを必ずしも行使するわけではありませんが、発信者のすべての可能な状態(つまり、その入力のすべての可能な値および変数)は計算的に実行不可能です。

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

あなたは文書化し、値が<= 0が渡されたときに、あなたの関数の動作をテストする場合、その、しかし注意してください負の値は、もはや無効でないにすべてのいずれかの引数があるよりも、少なくとも、これ以上の無効(throwそのため、例外をスローするように文書化されています!)。呼び出し元は、その防御的な動作に依存する権利があります。言語が許せば、これはどんな場合でも最良のシナリオかもしれません-関数に「無効な入力」がありませんが、例外をスローするように関数を引き起こさないと期待する呼び出し元は、それらが確実に行われないように十分にユニットテストされるべきですtは、それを引き起こす値を渡します。

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


悪い値を渡さないことを確認するために呼び出し元をテストすることに重点を置くそのビジネスは、多くの低音の厄介な依存関係を伴う脆弱なコードに役立ち、懸念の明確な分離はありません。私は、そのアプローチの背後にある考え方から生じるコードを望んでいないと本当に思います。
クレイグ

@Craig:依存関係をモックすることでテスト用のコンポーネントを分離した場合、このように見て、正しい値だけをそれらの依存関係に渡すことをテストしないのはなぜですか?また、コンポーネントを分離できない場合、懸念事項を本当に分離できましたか?防御的なコーディングには同意しませんが、防御的なチェックがコードの呼び出しの正確性をテストする手段である場合、それは混乱です。質問者の同僚はチェックが冗長であると正しいと思いますが、これを書かない理由として間違っていると思います:
スティーブジェソップ

私が見る唯一の明白な穴は、私自身のコンポーネントがそれらの依存関係に無効な値を渡せないことをテストしているだけです。パートナーがそれを呼び出すことができるようにコンポーネントを公開しますか?これは実際にデータベース設計、および現在のORMとの関係を思い出させており、その結果、多くの(ほとんど若い)人が、データベースは単なるネットワークストレージであり、制約、外部キー、ストアドプロシージャで自分自身を保護すべきではないと宣言しています。
クレイグ

私が見る他のことは、そのシナリオでは、もちろん、実際の依存関係ではなく、モックの呼び出しのみをテストしているということです。最終的には、呼び出し元のコードではなく、特定の渡された値で適切に動作できるかできないかの依存関係のコードです。そのため、依存関係は正しいことを行う必要があり、依存関係を十分に独立してテストする必要があります。私たちが話しているこれらのテストは「ユニット」テストと呼ばれることを忘れないでください。各依存関係はユニットです。:)
クレイグ

1

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

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

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


これの見出しと最初の段落は、実行時にコードを実行するのは単体テストではないため、真です。それは何だ、他のランタイムコードと現実世界の条件と悪いユーザ入力を変更し、ハッキングの試みは、コードと対話します。
クレイグ

1

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

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

特定のチェックが必要な機能である場合、いくつかのテストの存在がそれらを不必要にすることを断言することは確かに無意味です。パラメータ3が負のときに例外をスローすることが、ある機能の機能である場合、交渉できません。それを行うものとします。

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

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

同僚の考えには真実があり、彼が意味することはこれです:防御的にコーディングされ、すべての誤用に対して個別にテストされた非常に堅牢な低レベルのピースから関数を構築すると、高レベルの関数が可能になる可能性があります独自の広範なセルフチェックを行うことなく堅牢です。

その契約に違反した場合、おそらく例外などをスローすることにより、下位レベルの関数の誤用に変換されます。

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

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

TL; DR:あなたの同僚はおそらくばかではありません。要件が完全に特定されておらず、各自が「未記述の要件」が何であるかについて異なる考えを持っているため、あなたは同じことについて異なる観点でお互いに話し合っているだけです。パラメータチェックに特定の要件がない場合は、とにかく詳細なチェックをコーディングする必要があると思います。同僚は、パラメーターが間違っている場合に、堅牢な低レベルのコードを爆破させるだけだと考えています。コードを使用して未記述の要件について議論することは、生産的ではありません。コードよりも要件について意見が異なることを認識してください。コーディングの方法は、要件と思われるものを反映しています。同僚のやり方は、要件に対する彼の見解を表しています。そのように見れば、正しいか間違っているかは明らかではない コード自体のt。コードは、仕様がどうあるべきかについてのあなたの意見の単なるプロキシです。


これは、緩やかな要件である可能性のあるものを処理する一般的な哲学的困難と結びついています。不正な形式の入力が与えられた場合に、関数が有意であるが完全な自由支配ではなく任意に振る舞うことが許可されている場合(たとえば、イメージデコーダーが、その余暇に、任意のピクセルの組み合わせを生成するか、異常終了することが保証される場合に要件を満たす場合) 、ただし悪意を持って作成された入力が任意のコードを実行できる可能性がある場合はそうではありません)、どのテストケースが受け入れられない動作を生成しないことを保証するのに適切かは不明です。
-supercat

1

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

当然の結果として、存在しない試験のは、定義含ま契約未定義の動作を。したがって、に渡さnullFoo::Frobnicate(Widget widget)、実行時の大混乱が続いても、あなたはクラスの契約内にあります。

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

そして、あなたは

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

1

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

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

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


0

TDDのテストは、コードの開発間違いを発見します

防御的プログラミングの一部として記述する境界チェックは、コードの使用ミスをキャッチします

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


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

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

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


1
私は下票しませんでしたが、この種の議論に微妙な区別を加えることは水を汚すという前提で下票に同意します。
クレイグ

@Craig私が追加した特定の例についてのフィードバックに興味があります。
ブラックホーク

例の特異性が気に入っています。私がまだ抱えている唯一の懸念は、議論全体に一般的です。例えば; チームに新しい開発者が来て、その金融モジュールを使用する新しいコンポーネントを作成します。新しい男は、システムの複雑さのすべてを認識しているわけではなく、システムの動作方法に関するあらゆる種類の専門知識が、テスト対象のコードではなくテストに組み込まれているという事実は言うまでもありません。
クレイグ

したがって、新しいガイ/ギャルはいくつかの重要なテストを作成できず、テストの冗長性が発生します。システムの異なる部分のテストは同じ条件をチェックしており、適切なアサーションやアクションがあるコードの前提条件チェック。
クレイグ

1
そんな感じ。ここでの引数の多くは、呼び出し元のコードのテストですべてのチェックを行うことに関するものでした。しかし、ある程度のファンインがある場合、最終的にはさまざまな場所から同じチェックを行うことになり、それ自体がメンテナンスの問題になります。プロシージャの有効な入力の範囲が変更されたが、さまざまなコンポーネントを実行するテストにその範囲のドメイン知識が組み込まれている場合はどうなりますか?私はまだ完全に防御的なプログラミングを支持しており、プロファイリングを使用して、パフォーマンスの問題に対処する必要があるかどうかを判断します。
クレイグ

0

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

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

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

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


これは、面接での質問の良い例のようです(戻り値と「出力」パラメーターsumがあるのはなぜですか?また、nullポインターの場合はどうなりますか?)
トビースパイト
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.