web-dev-qa-db-ja.com

すべてを公開してスタブおよびモックする単体テストのポイントはありますか?

ユニットテストを「適切な」方法で行う場合、つまり、すべてのパブリックコールをスタブしてプリセット値またはモックを返す場合、実際には何もテストしていないように感じます。文字通りコードを見て、パブリックメソッドを介したロジックのフローに基づいて例を作成しています。また、実装が変更されるたびに、これらのテストを行って変更する必要があります。繰り返しますが、(中長期にかかわらず)何か有用なことを達成しているとは感じていません。また、統合テスト(非幸せパスを含む)も行っており、テスト時間の増加を気にしていません。それらを使用すると、実際にリグレッションをテストしているように感じます。これは、リグレッションが複数回検出されたためです。一方、ユニットテストで行うことは、パブリックメソッドの実装が変更されたことを示しています。

ユニットテストは広大なトピックであり、私はここで何かを理解していないような気がします。単体テストと統合テストの決定的な利点は何ですか(時間のオーバーヘッドを除く)?

60
enthrops

ユニットテストを「適切な」方法で行う場合、つまり、すべてのパブリックコールをスタブしてプリセット値またはモックを返す場合、実際には何もテストしていないように感じます。文字通りコードを見て、パブリックメソッドを介したロジックのフローに基づいて例を作成しています。

これは、テストしているメソッドが他のいくつかのクラスインスタンス(モックする必要がある)を必要とし、独自にいくつかのメソッドを呼び出すように聞こえます。

このタイプのコードは、概説する理由により、単体テストが実際に困難です。

私が役に立ったと思ったのは、そのようなクラスを次のように分割することです。

  1. 実際の「ビジネスロジック」を持つクラス。これらは他のクラスへの呼び出しをほとんどまたはまったく使用せず、テストが簡単です(値の入力-値の出力)。
  2. 外部システム(ファイル、データベースなど)とのインターフェースとなるクラス。これらは外部システムをラップし、ニーズに合わせて便利なインターフェイスを提供します。
  3. 「すべてをつなぐ」クラス

その後、1。のクラスは、値を受け入れて結果を返すだけなので、単体テストが簡単です。より複雑なケースでは、これらのクラスは独自に呼び出しを実行する必要があるかもしれませんが、2からのクラスのみを呼び出します(たとえば、データベース関数を直接呼び出すことはありません)。必要なラップされたシステムの部分を公開します)。

2.と3.のクラスは通常、意味のある単体テストを行うことはできません(これらのクラスはそれ自体では何も有用ではないため、単なる「接着剤」コードです)。 OTOH、これらのクラスは比較的単純な(そして少数の)傾向があるため、統合テストで十分にカバーする必要があります。


1つのクラス

データベースから価格を取得し、割引を適用して、データベースを更新するクラスがあるとします。

これをすべて1つのクラスで実行する場合は、DB関数を呼び出す必要があります。擬似コード:

_1 select price from database
2 perform price calculation, possibly fetching parameters from database
3 update price in database
_

3つのステップすべてでDBアクセスが必要になるため、多くの(複雑な)モッキングが発生します。これは、コードまたはDB構造が変更されると壊れる可能性があります。

分割

PriceCalculation、PriceRepository、Appの3つのクラスに分割します。

PriceCalculationは実際の計算のみを行い、必要な値を提供します。アプリはすべてを結び付けます:

_App:
fetch price data from PriceRepository
call PriceCalculation with input values
call PriceRepository to update prices
_

そのように:

  • PriceCalculationは「ビジネスロジック」をカプセル化します。それ自体は何も呼び出さないため、テストは簡単です。
  • PriceRepositoryは、モックデータベースをセットアップし、読み取りと更新の呼び出しをテストすることで、疑似ユニットテストを実行できます。ロジックがほとんどないため、コードパスが少ないため、これらのテストはあまり必要ありません。
  • アプリはグルーコードであるため、意味のある単体テストはできません。ただし、これも非常に単純なので、統合テストで十分です。後でアプリが複雑になりすぎると、さらに多くの「ビジネスロジック」クラスが発生します。

最後に、PriceCalculationが独自のデータベース呼び出しを実行する必要があることが判明する場合があります。たとえば、PriceCalculationだけが必要なデータを認識しているため、Appが事前にフェッチすることはできません。次に、PriceCalculationのニーズに合わせてカスタマイズされたPriceRepository(または他のリポジトリクラス)のインスタンスを渡すことができます。このクラスはモックする必要がありますが、PriceRepositoryのインターフェースが単純なため、これは単純です。 PriceRepository.getPrice(articleNo, contractType)。最も重要なのは、PriceRepositoryのインターフェースがPriceCalculationをデータベースから分離するため、DBスキーマまたはデータ構成を変更してもインターフェースが変更されないため、モックが壊れることはほとんどありません。

38
sleske

単体テストと統合テストの決定的な利点は何ですか?

それは誤った二分法です。

単体テストと統合テストは、似ていますが異なる目的で使用されます。単体テストの目的は、メソッドが機能することを確認することです。実際には、ユニットテストは、コードがユニットテストで概説されている契約を満たしていることを確認します。これは、ユニットテストの設計方法から明らかです。 :それらはコードが何をすることになっているのかを明確に述べ、assertコードがそれを行うことを示します。

統合テストは異なります。統合テストは、ソフトウェアコンポーネント間の相互作用を実行します。すべてのテストに合格しても、適切に相互作用しないために統合テストに失敗するソフトウェアコンポーネントを使用できます。

ただし、単体テストに決定的な利点がある場合、これは次のようになります。単体テストは設定がはるかに簡単で、統合テストよりもはるかに少ない時間と労力で済みます。適切に使用すると、単体テストは「テスト可能な」コードの開発。これは、最終結果がより信頼性が高く、理解しやすく、保守しやすくなることを意味します。テスト可能なコードには、一貫性のあるAPI、反復可能な動作などの特定の特性があり、アサートしやすい結果を返します。

複雑なモック、複雑なセットアップ、難しいアサーションが必要になることが多いため、統合テストはより困難でコストがかかります。システム統合の最高レベルで、UIで人間の相互作用をシミュレートしようとしていると想像してください。ソフトウェアシステム全体が、この種の自動化に専念しています。そして私たちが求めているのは自動化です。人間によるテストは再現性がなく、自動テストのように拡張できません。

最後に、統合テストはコードカバレッジを保証しません。統合テストでテストするコードループ、条件、分岐の組み合わせはいくつありますか?あなたは本当に知っていますか?単体テストとテスト対象のメソッドで使用できるツールがあり、コードカバレッジの量と、コードの循環的複雑度を知ることができます。ただし、実際に機能するのは、単体テストが存在するメソッドレベルのみです。


リファクタリングのたびにテストが変化する場合、それは別の問題です。ユニットテストは、ソフトウェアが何をするかを文書化し、それを実行することを証明し、次にそれを実行することを証明することを想定していますagain実装。 APIが変更された場合、またはシステム設計の変更に応じてメソッドを変更する必要がある場合、それが起こるはずです。頻繁に発生する場合は、コードを記述する前に、まずテストを記述することを検討してください。これにより、アーキテクチャ全体について検討し、すでに確立されているAPIを使用してコードを記述します。

次のような簡単なコードの単体テストを作成するのに多くの時間を費やしている場合

public string SomeProperty { get; set; }

その後、アプローチを再検討する必要があります。ユニットテストでは動作をテストすることになっており、上記のコード行には動作がありません。ただし、コード内のどこかで依存関係が作成されています。そのプロパティはほぼ確実にコード内のどこかで参照されるためです。そうする代わりに、必要なプロパティをパラメータとして受け入れるメソッドを書くことを検討してください:

public string SomeMethod(string someProperty);

これで、メソッドにはそれ自体の外の何かへの依存関係がなくなり、メソッドは完全に自己完結型であるため、よりテストしやすくなりました。確かに、これを常に実行できるわけではありませんが、コードはよりテストしやすい方向に移動します。今回は、実際の動作の単体テストを作成しています。

27
Robert Harvey

モックを使用した単体テストでは、クラスの実装が正しいことを確認します。テストするコードの依存関係のパブリックインターフェイスをモックします。このようにして、クラスの外部にあるすべてのものを制御し、失敗したテストが他のオブジェクトの1つではなく、クラスの内部の何かによるものであることを確認します。

また、実装ではなく、テスト中のクラスの動作をテストします。コードをリファクタリングする場合(新しい内部メソッドの作成など)、単体テストは失敗しないはずです。しかし、パブリックメソッドの動作を変更する場合、動作を変更したため、テストは完全に失敗するはずです。

コードを書いた後にテストを書いているようにも聞こえますが、代わりに最初にテストを書いてみてください。クラスが持つべき振る舞いを概説してから、テストに合格させるためにminimumの量のコードを記述します。

単体テストと統合テストはどちらも、コードの品質を保証するのに役立ちます。単体テストでは、各コンポーネントを個別に調べます。統合テストでは、すべてのコンポーネントが適切に相互作用することを確認します。テストスイートに両方のタイプを含めたいです。

一度に1つのアプリケーションに集中できるので、ユニットテストは開発に役立ちました。まだ作成していないコンポーネントをモックする。彼らはまた、私が見つけたロジックのバグを文書化しているので(ユニットテストでも)、回帰に最適です。

[〜#〜]更新[〜#〜]

メソッドが呼び出されることを確認するだけのテストを作成することには、メソッドが実際に呼び出されることを確認するという意味があります。特に、最初にテストを作成する場合は、実行する必要があるメソッドのチェックリストがあります。このコードはほとんど手続き型であるため、メソッドが呼び出されること以外にチェックする必要はありません。あなたは将来の変更のためにコードを保護しています。 1つのメソッドを他のメソッドの前に呼び出す必要がある場合。または、最初のメソッドが例外をスローしても、メソッドは常に呼び出されます。

このメソッドのテストは変更されないか、メソッドを変更しているときにのみ変更される場合があります。なぜこれは悪いことなのですか?テストの使用を強化するのに役立ちます。コードを変更した後でテストを修正する必要がある場合は、コードでテストを変更する習慣を身に付けます。

4
Schleis

私は同様の質問を経験していました-コンポーネントテストの力を発見するまで。簡単に言うと、デフォルトではモックしないが、実際にはオブジェクトを使用することを除いて(理想的には依存性注入を介して)、単体テストと同じです。

このようにして、優れたコードカバレッジで堅牢なテストをすばやく作成できます。常にモックを更新する必要はありません。モックが100%の単体テストよりも少し正確ではないかもしれませんが、節約する時間とお金でそれを補います。モックまたはフィクスチャを実際に使用する必要があるのは、ストレージバックエンドまたは外部サービスだけです。

実際、過度のモックはアンチパターンです TDD Anti-Patterns および Mocks is evil

3
lastzero