52

最近、ユニットテストについて同僚と興味深い議論をしました。契約が変更されたときに、単体テストの保守の生産性が低下したときについて話し合っていました。

おそらく、誰でもこの問題に取り組む方法を教えてくれるでしょう。詳しく説明しましょう:

ですから、気の利いた計算を行うクラスがあるとしましょう。コントラクトは、数値を計算する必要があると述べています。または、何らかの理由で失敗した場合は -1 を返します。

私はそれをテストする契約テストを持っています。そして、他のすべてのテストでは、この気の利いた電卓をスタブします。

したがって、契約を変更すると、計算できないときはいつでも CannotCalculateException がスローされます。

私の契約テストは失敗するので、それに応じて修正します。ただし、モック/スタブ化されたすべてのオブジェクトは、古いコントラクト ルールを引き続き使用します。これらのテストは成功しますが、成功するはずはありません!

ここで問題になるのは、単体テストに対するこの信頼を基に、そのような変更をどれだけ信頼できるかということです... 単体テストは成功しますが、アプリケーションのテスト中にバグが発生します。この計算機を使用するテストは修正する必要があります。これには時間がかかり、何度もスタブ/モックされることさえあります...

このケースについてどう思いますか?私はそれについて深く考えたことはありませんでした。私の意見では、単体テストに対するこれらの変更は受け入れられるでしょう。単体テストを使用しないと、テスト段階で (テスターに​​よって) そのようなバグが発生することもわかります。しかし、何がより多くの時間 (またはより短い時間) を要するかを指摘できるほど自信がありません。

何かご意見は?

4

9 に答える 9

92

あなたが提起する最初の問題は、いわゆる「脆弱なテスト」の問題です。アプリケーションに変更を加えると、その変更のために何百ものテストが失敗します。これが発生すると、設計上の問題が発生します。テストは壊れやすいように設計されています。それらは、実動コードから十分に分離されていません。解決策は(このようなすべてのソフトウェアの問題でそうであるように)、本番コードの揮発性がテストから隠されるように、テストを本番コードから切り離す抽象化を見つけることです。

この種の脆弱性を引き起こすいくつかの単純なものは次のとおりです。

  • 表示される文字列をテストします。このような文字列は、アナリストの気まぐれで文法やスペルが変わる可能性があるため、不安定です。
  • 抽象化(例:FULL_TIME)の背後でエンコードする必要がある離散値(例:3)のテスト。
  • 多くのテストから同じAPIを呼び出す。API呼び出しをテスト関数でラップして、APIが変更されたときに1か所で変更できるようにする必要があります。

テスト設計は、TDDの初心者がしばしば無視する重要な問題です。これにより、テストが脆弱になることが多く、初心者はTDDを「非生産的」として拒否することになります。

あなたが提起した2番目の問題は誤検知でした。非常に多くのモックを使用したため、どのテストも実際に統合システムをテストしていません。独立したユニットをテストすることは良いことですが、システムの部分的および全体的な統合をテストすることも重要です。TDDは単体テストだけではありません。

テストは次のように配置する必要があります。

  • 単体テストは、ほぼ100%のコードカバレッジを提供します。彼らは独立したユニットをテストします。それらは、システムのプログラミング言語を使用してプログラマーによって書かれています。
  • コンポーネントテストは、システムの約50%をカバーします。それらはビジネスアナリストとQAによって書かれています。これらは、FitNesse、Selenium、Cucumberなどの言語で記述されています。個々のユニットではなく、コンポーネント全体をテストします。彼らは主にハッピーパスのケースといくつかの非常に目に見える不幸なパスのケースをテストします。
  • 統合テストは、システムの約20%をカバーします。システム全体ではなく、コンポーネントの小さなアセンブリをテストします。FitNesse / Selenium/Cucumberなどでも書かれています。建築家によって書かれました。
  • システムテストは、システムの約10%をカバーします。彼らは一緒に統合されたシステム全体をテストします。ここでも、FitNesse / Selenium/Cucumberなどで書かれています。建築家によって書かれました。
  • 探索的手動テスト。(James Bachを参照)これらのテストは手動ですが、スクリプト化されていません。彼らは人間の創意工夫と創造性を採用しています。
于 2010-06-03T20:08:32.557 に答える
12

意図的なコード変更が原因で失敗する単体テストを修正する必要があるのは、これらの変更によって最終的に発生するバグをキャッチするためのテストがない場合よりも優れています。

コードベースの単体テストカバレッジが良好な場合、コードのバグではなく、コントラクトまたはコードリファクタリングの意図的な変更が原因で、多くの単体テストの失敗に遭遇する可能性があります。

ただし、その単体テストカバレッジにより、コードをリファクタリングし、契約の変更を実装する自信も得られます。一部のテストは失敗し、修正する必要がありますが、他のテストは、これらの変更で導入したバグが原因で最終的に失敗します。

于 2010-06-03T11:54:20.193 に答える
5

ユニットテストは、100%のコード/機能カバレッジの理想的な場合でも、すべてのバグを確実にキャッチできるわけではありません。それは予想外だと思います。

テストされた契約が変更された場合、私(開発者)は頭脳を使ってすべてのコード(テストコードを含む!)を適宜更新する必要があります。いくつかのモックを更新しなかった場合でも、古い動作が発生します。これは、単体テストではなく、私のせいです。

これは、バグを修正して単体テストを作成する場合と似ていますが、すべての同様のケースを検討(およびテスト)することができず、後でバグが発生するケースもあります。

そうです、単体テストには、製品コード自体と同様にメンテナンスが必要です。メンテナンスなしで、彼らは腐敗して腐敗します。

于 2010-06-03T11:51:35.477 に答える
4

単体テストについても同様の経験があります。あるクラスのコントラクトを変更する場合、他のテストの負荷も変更する必要があります(実際には多くの場合合格するため、さらに困難になります)。そのため、私は常に高レベルのテストも使用しています。

  1. 受け入れテスト-2つ以上のクラスをテストします。これらのテストは通常​​、実装する必要のあるユーザーストアに合わせて調整されます。したがって、ユーザーストーリーが「機能する」ことをテストします。これらはDBや他の外部システムに接続する必要はありませんが、接続することはできます。
  2. 統合テスト-主に外部システムの接続性などをチェックします。
  3. 完全なエンドツーエンドテスト-システム全体をテストします

ユニットテストカバレッジが100%であっても、アプリケーションの起動が保証されるわけではないことに注意してください。そのため、より高いレベルのテストが必要です。テストの層が非常に多いのは、テストが低いほど、通常は安価になるためです(開発、テストインフラストラクチャの維持、および実行時間の観点から)。

補足として、単体テストを使用して言及した問題のために、コンポーネントを可能な限り分離し、それらのコントラクトを可能な限り小さく保つことを教えています。これは間違いなく良い習慣です!

于 2010-06-03T11:56:00.203 に答える
3

ある人がGoogle グループで、「Growing Object Oriented Software - Guided by Tests」という本について同じ質問をしました。スレッドは単体テストのモック/スタブの仮定の腐敗です。

これがJB Rainsberger の回答です (彼は Manning の「JUnit Recipes」の著者です)。

于 2012-04-11T07:49:53.807 に答える
2

単体テスト コード (およびテストに使用される他のすべてのコード) の規則の 1 つは、製品コードと同じように扱うことです。

これについての私の理解では、(関連性を維持し、リファクタリングし、本番コードのように機能させることに加えて)、投資/コストの見通しからも同じように見なす必要があります。

おそらく、テスト戦略には、最初の投稿で説明した問題に対処するための何かを含める必要があります-デザイナーが変更されたときに、どのテストコード (スタブ/モックを含む) をレビュー (実行、検査、変更、修正など) する必要があるかを指定する行に沿ったものプロダクション コードの関数/メソッド。したがって、製品コードの変更のコストには、これを行うためのコストを含める必要があります。そうしないと、テスト コードは「第 3 級市民」になり、単体テスト スイートに対する設計者の信頼とその関連性が低下します...明らかに、ROI はバグの発見と修正のタイミングにあります。

于 2010-06-03T18:21:30.887 に答える
1

ここで私が頼りにしている原則の 1 つは、重複を取り除くことです。私は通常、このコントラクトを実装する多くの異なる偽物やモックを持っていません (この理由の一部として、モックよりも多くの偽物を使用しています)。コントラクトを変更すると、そのコントラクト、製品コード、またはテストのすべての実装を検査するのが自然です。この種の変更を行っていることに気付いたとき、それは私を悩ませます、私の抽象化はおそらくもっとよく考えられていたはずですが、契約の変更の規模のためにテストコードを変更するにはあまりにも面倒な場合は、自分自身に尋ねなければなりませんこれらもリファクタリングによるものです。

于 2010-06-03T23:54:18.200 に答える
0

私はそれをこのように見ています、あなたの契約が変わるとき、あなたはそれを新しい契約のように扱うべきです。したがって、この「新しい」コントラクトのUNITテストのまったく新しいセットを作成する必要があります。あなたが既存のテストケースのセットを持っているという事実は、要点を超えています。

于 2010-06-03T11:52:52.400 に答える
0

問題はデザインにあるというボブおじさんの意見を2番目に言います。さらに一歩戻って、契約のデザインを確認します

要するに

「x==0の場合は-1を返す」または「x==yの場合はCannotCalculateExceptionをスローする」と言う代わりに niftyCalcuatorThingy(x,y)適切な状況で前提条件を指定x!=y && x!=0してください(以下を参照)。したがって、スタブはこれらのケースで任意に動作する可能性があり、単体テストはそれを反映する必要があります。つまり、契約やテストを変更することなく、最大のモジュール性、つまり、指定されていないすべてのケースでテスト対象システムの動作を任意に変更する自由があります。

必要に応じて仕様不足

次の基準に従って、ステートメント「何らかの理由で失敗した場合は-1」を区別できます。シナリオですか

  1. 実装がチェックできる例外的な動作?
  2. メソッドのドメイン/責任の範囲内ですか?
  3. 呼び出し元(または呼び出しスタックの初期の誰か)が他の方法で回復/処理できるという例外はありますか?

1)から3)が成立する場合に限り、コントラクトでシナリオを指定します(たとえばEmptyStackException、空のスタックでpop()を呼び出すときにスローされます)。

1)がないと、例外的な場合に実装が特定の動作を保証することはできません。たとえば、Object.equals()は、反射性、対称性、推移性、および一貫性の条件が満たされない場合の動作を指定しません。

2)がないと、SingleResponsibilityPrincipleが満たされず、モジュール性が失われ、コードのユーザー/リーダーが混乱します。たとえば、深く、シリアル化によるクローン作成が行われるため、スローされる可能性があることGraph transform(Graph original)を指定しないでください。MissingResourceException

3)がないと、呼び出し元は指定された動作(特定の戻り値/例外)を利用できません。たとえば、JVMがUnknownErrorをスローした場合です。

長所と短所

1)、2)、または3)が当てはまらない場合を指定すると、いくつかの問題が発生します。

  • (設計者による)契約の主な目的はモジュール性です。これは、責任を実際に分離する場合に最もよく達成できます。前提条件(呼び出し元の責任)が満たされていない場合、実装の動作を指定しないと、例が示すように、モジュール性が最大になります。
  • 将来、例外をスローするメソッドのより一般的な機能にさえも、変更する自由はありません。
  • 例外的な動作は非常に複雑になる可能性があるため、それらを対象とする契約は複雑になり、エラーが発生しやすく、理解しにくくなります。たとえば、すべての状況がカバーされていますか?複数の例外的な前提条件が成立する場合、どの動作が正しいですか?

仕様不足の欠点は、(テスト)堅牢性、つまり、異常な状態に適切に対応する実装の能力が難しいことです。

妥協案として、可能な場合は次のコントラクトスキーマを使用します。

<(半)正式なPREおよびPOST条件、1)から3)が成立する例外的な動作を含む>

PREが満たされない場合、現在の実装はRTE A、B、またはCをスローします。

于 2012-10-07T16:26:02.657 に答える