(なぜ)単体テストが依存関係をテストしないことが重要ですか?


103

自動化されたテストの価値を理解し、問題が十分に特定されていて、良いテストケースを思いつくことができる場所ならどこでもそれを使用します。ただし、こことStackOverflowの一部の人々は、依存関係ではなくユニットのみをテストすることを強調していることに気付きました。ここでは、利益が見られません。

テストの依存関係を回避するためのモッキング/スタブ化は、テストを複雑にします。実稼働コードに人為的な柔軟性/デカップリング要件を追加して、モックをサポートします。(これは良いデザインを促進すると言う人には賛成できません。余分なコードを書いたり、依存性注入フレームワークのようなものを導入したり、コードベースに複雑さを加えて実際のユースケースなしで物事をより柔軟/プラグ可能/拡張可能/分離することは、オーバーエンジニアリングではなく、良いデザイン。)

第二に、依存関係のテストとは、どこでも使用される重要な低レベルコードが、テストを明示的に考えた人以外の入力でテストされることを意味します。依存する低レベルの機能をモックアウトせずに、高レベルの機能で単体テストを実行することで、低レベルの機能に多くのバグを発見しました。理想的には、これらは低レベル機能の単体テストで発見されたはずですが、見逃されたケースは常に発生します。

これの反対側は何ですか?単体テストが依存関係もテストしないことは本当に重要ですか?もしそうなら、なぜですか?

編集:データベース、ネットワーク、Webサービスなどの外部依存関係のモックの価値を理解できます(これを明確にする動機付けをしてくれたAnna Learに感謝します)。内部依存関係、つまり他のクラス、静的関数などに言及していました。直接的な外部依存関係はありません。


14
「オーバーエンジニアリング、良いデザインではない」。それ以上の証拠を提供する必要があります。多くの人はそれを「オーバーエンジニアリング」ではなく「ベストプラクティス」と呼んでいます。
-S.Lott

9
@ S.Lott:もちろんこれは主観的なものです。コードをモック可能にすると良いデザインが促進されるため、問題を引き起こし、モックが良いと言う答えがたくさん欲しくありません。ただし、一般的には、現在または近い将来に明らかな利点のない方法で分離されたコードを扱うのは嫌です。現在、複数の実装がなく、近い将来に実装されることを予期していない場合は、ハードコーディングする必要があります。それはより単純で、オブジェクトの依存関係の詳細をクライアントに飽きさせません。
dsimcha

5
ただし、一般的に、デザインが貧弱であることを除いて明白な根拠がない方法で結合されたコードを扱うのは嫌です。それはより単純で、クライアントに個別にテストする柔軟性を退屈させません。
S.Lott

3
@ S.Lott:明確化:明確なユースケースなしで物事を切り離すために、特にコードまたはそのクライアントコードを大幅に冗長にし、さらに別のクラス/インターフェイスを導入する場合、コードが意味をなさないようになります。もちろん、コードを最も単純で最も簡潔な設計よりも密に結合することは求めていません。また、抽象化の線を時期尚早に作成すると、通常は間違った場所に配置されます。
-dsimcha

行っている区別を明確にするために質問を更新することを検討してください。一連のコメントを統合することは困難です。してください更新、それを明確にし、集中する質問を。
-S.ロット

回答:


116

定義の問題です。依存関係のあるテストは、単体テストではなく統合テストです。統合テストスイートも必要です。違いは、統合テストスイートが別のテストフレームワークで実行される可能性があり、おそらく時間がかかるためビルドの一部としてではない可能性があることです。

製品の場合:ユニットテストはビルドごとに実行され、数秒かかります。統合テストのサブセットは、チェックインごとに実行され、10分かかります。完全な統合スイートは毎晩実行され、4時間かかります。


4
発見され修正されたバグをカバーするリグレッションテストを忘れずに、将来のメンテナンス中にシステムに再導入されるのを防ぎます。
-RBerteig

2
定義に同意しても、私はその定義を主張しません。一部のコードには、StringFormatterのような許容可能な依存関係がありますが、それでもほとんどの単体テストで考慮されています。
ダニダカル

2
danip:明確な定義が重要であり、私それら主張します。しかし、これらの定義がターゲットであることを認識することも重要です。狙う価値はありますが、必ずしもブルズアイが必要なわけではありません。単体テストは、多くの場合、下位レベルのライブラリに依存します。
ジェフリーファウスト

2
また、ファサードテストの概念を理解することも重要です。ファサードテストは、コンポーネントがどのように適合するか(統合テストなど)をテストするのではなく、単一のコンポーネントを単独でテストします。(すなわち、オブジェクトグラフ)
リカルドロドリゲス14

1
速くなるだけでなく、非常に退屈で管理しにくいということを想像してください。そうしないと、モッキングの実行ツリーが指数関数的に成長する可能性があります。モックをさらにプッシュすると、ユニットで10個の依存関係をモックすると想像してください。たとえば、10個の依存関係の1つが多くの場所で使用され、20個の独自の依存関係があるため、多くの場所で重複する可能性のある他の20個の依存関係をモックする必要があります。最終APIポイントでは、データベースなどをモックする必要があります。そのため、データベースをリセットする必要がなく、
先ほど

39

すべての依存関係を適切にテストすることは依然として重要ですが、ジェフリーファウストが言ったように、それは統合テストの領域にあります。

単体テストの最も重要な側面の1つは、テストを信頼できるものにすることです。合格したテストが本当に良いことを意味し、失敗したテストが実際に本番コードの問題を意味することを信用しない場合、テストは可能な限り有用ではありません。

テストを信頼できるものにするためには、いくつかのことを行う必要がありますが、この回答では1つだけに焦点を当てます。もし開発者のすべてが簡単にコードをチェックインする前にそれらを実行できるように、彼らは実行しやすいことを確認する必要があります。「簡単に実行することは、」あなたのテストはすぐに実行することを意味し、それらを行かせるために必要な一切の広範な構成や設定がありません。理想的には、誰でもコードの最新バージョンをチェックアウトし、すぐにテストを実行し、合格することを確認できるはずです。

他のもの(ファイルシステム、データベース、Webサービスなど)への依存関係を抽象化することで、構成の必要性を回避でき、あなたや他の開発者は「ああ、テストは失敗したから」と言いたくなる状況になりにくくなりますネットワーク共有を設定します。まあ、後で実行します。」

一部のデータを使用して何を行うかをテストする場合、そのビジネスロジックコードの単体テストでは、そのデータの取得方法を考慮する必要はありません。データベースなどのサポートに依存せずにアプリケーションのコアロジックをテストできることは素晴らしいことです。あなたがそれをしていないなら、あなたは逃しています。

PSテスト可能性という名目でオーバーエンジニアリングすることは間違いなく可能であると付け加えます。アプリケーションのテスト駆動は、それを軽減するのに役立ちます。しかし、いずれにしても、アプローチの実装が不十分であっても、アプローチの有効性が低下することはありません。「なぜ私はこれをしているのですか?」開発中。


内部の依存関係に関する限り、物事は少し濁っています。私がそれについて考えるのが好きなのは、間違った理由でクラスが変わるのを可能な限り防ぐことです。このような設定があれば...

public class MyClass 
{
    private SomeClass someClass;
    public MyClass()
    {
        someClass = new SomeClass();
    }

    // use someClass in some way
}

私は通常、どのようSomeClassに作成されるかを気にしません。使いたいだけです。SomeClassが変更され、コンストラクターへのパラメーターが必要になった場合、それは私の問題ではありません。これに対応するためにMyClassを変更する必要はないはずです。

さて、それは設計部分に触れているだけです。単体テストに関する限り、他のクラスからも自分を守りたいと思っています。MyClassをテストする場合、外部依存関係がないこと、SomeClassがデータベース接続または他の外部リンクを導入していないことを知っているのが好きです。

しかし、さらに大きな問題は、メソッドの結果の一部がSomeClassのメソッドの出力に依存していることも知っていることです。SomeClassをモック/スタブ化せずに、その入力を需要に応じて変更する方法がないかもしれません。運が良ければ、SomeClassから正しい応答をトリガーするようにテスト内で環境を構成できるかもしれませんが、そのようにすると、テストが複雑になり、テストが脆弱になります。

MyClassを書き換えてコンストラクターでSomeClassのインスタンスを受け入れると、(モックフレームワークまたは手動モックを使用して)必要な値を返すSomeClassの偽のインスタンスを作成できます。私は、一般的にはありません持っている。この場合、インタフェースを導入します。そうするかどうかは、多くの点で選択する言語によって決定される個人的な選択です(たとえば、インターフェイスはC#でより可能性が高いですが、Rubyでは間違いなく必要ではありません)。


+1すばらしい答え。外部依存関係は、元の質問を書いたときに考えていなかったケースだからです。質問に対する回答を明確にしました。最新の編集をご覧ください。
-dsimcha

@dsimcha私は答えを拡張して、それについてさらに詳しく説明しました。それが役に立てば幸い。
アダムリア

アダム、「SomeClassが変更され、コンストラクターへのパラメーターが必要になったら... MyClassを変更する必要はない」という言葉が正しい言語を提案できますか?申し訳ありませんが、それは異常な要件として私に飛び出しました。MyClassの単体テストを変更する必要はありませんが、MyClassを変更する必要はありません...うわー。

1
@moz私が意図したことは、SomeClassが作成される方法があれば、SomeClassが内部で作成される代わりに挿入される場合、MyClassは変更する必要がないということでした。通常の状況では、MyClassはSomeClassのセットアップの詳細を気にする必要はありません。SomeClassのインターフェースが変更された場合、ええ... MyClassが使用するメソッドのいずれかに影響がある場合、MyClassを変更する必要があります。
アダムリア

1
MyClassのテスト時にSomeClassをモックした場合、MyClassがSomeClassを誤って使用しているか、変更される可能性のあるテストされていないSomeClassの癖に依存しているかをどのように検出しますか?
名前は

22

単体テストと統合テストの問題は別として、次のことを考慮してください。

クラスウィジェットは、クラスThingamajigおよびWhatsItに依存しています。

ウィジェットの単体テストが失敗します。

どのクラスに問題がありますか?

「デバッガを起動する」または「コードが見つかるまでコードを読む」と回答した場合、依存関係ではなくユニットのみをテストすることの重要性を理解しています。


3
@Brook:ウィジェットのすべての依存関係の結果を確認してください。それらがすべて合格した場合、別の方法で証明されるまで、それはウィジェットの問題です。
-dsimcha

4
@dsimchaですが、今はゲームに複雑さを追加して、中間ステップを確認しています。単純化して、単純な単体テストを最初に実行してみませんか?次に、統合テストを行います。
asoundmove

1
@dsimcha。これは、重要な依存関係グラフに到達するまでは理にかなっています。依存関係のある3層以上の深さの複雑なオブジェクトがあるとします。O(1)ではなく、O(N ^ 2)検索の問題になります
ブルック

1
また、SRPの私の解釈では、クラスは概念/問題ドメインレベルで単一の責任を負うべきであるとしています。その責任を果たすために、より低い抽象レベルで見たときに多くの異なることをする必要があるかどうかは関係ありません。ほとんどのクラスがデータを保持し、それに対して操作を実行するため(十分に低いレベルで表示した場合、2つのこと)、極端にとるとSRPはオブジェクト指向に反します。
-dsimcha

4
@Brook:タイプが増えてもそれ自体は複雑になりません。それらのタイプが問題の領域で概念的に意味を持ち、コードを理解するのを難しくせずに容易にする場合、より多くのタイプは問題ありません。問題は、問題ドメインレベルで概念的に結合されたものを人為的に分離することです(つまり、実装が1つだけで、おそらく複数あることはないなど)。また、分離は問題ドメインの概念にうまくマッピングされません。このような場合、この実装を真剣に抽象化するのはばかげて、官僚的で冗長です。
-dsimcha

14

プログラミングは料理のようなものだと想像してください。単体テストは、材料が新鮮でおいしいことを確認することと同じです。一方、統合テストは、食事がおいしいことを確認するようなものです。

最終的には、食事がおいしいこと(またはシステムが機能すること)を確認することが最も重要なことであり、究極の目標です。しかし、もしあなたの材料、つまりユニットが機能していれば、もっと安い方法で見つけることができます。

実際、ユニット/メソッドが機能することを保証できれば、システムが機能している可能性が高くなります。「確実」ではなく「可能性が高い」ことを強調します。あなたはまだあなたが調理した食事を味わい、最終製品が良いことを伝えるために誰かが必要であるのと同じように、あなたはまだ統合テストが必要です。新鮮な食材を使って簡単に到着できます。それだけです。


7
そして、統合テストが失敗すると、ユニットテストは問題に焦点を合わせることができます。
ティムウィリスクロフト

9
類推を伸ばすために:あなたの食事がひどいものであることがわかったとき、それはあなたのパンがカビの生えたものであると判断するためにいくらかの調査を必要とするかもしれません。ただし、その結果、ユニットテストを追加して、調理前にカビの生えたパンをチェックし、その特定の問題が再び発生しないようにします。その後、別の食事が失敗した場合、原因としてカビの生えたパンを排除しました。
キラレッサ

この類推が大好き!
thehowler

6

ユニットを完全に分離してテストすることで、このユニットが送信できるデータと状況のすべての可能なバリエーションをテストできます。残りから隔離されているため、テスト対象のユニットに直接影響を与えないバリエーションは無視できます。これにより、テストの複雑さが大幅に軽減されます。

さまざまなレベルの依存関係を統合したテストにより、単体テストではテストされなかった可能性のある特定のシナリオをテストできます。

両方が重要です。単体テストを実行するだけで、コンポーネントを統合するときに常により複雑な微妙なエラーが発生します。統合テストを行うということは、マシンの個々の部品がテストされていないという自信なしにシステムをテストしていることを意味します。エントリの組み合わせの数を追加するコンポーネントが非常に高速になり(要因的と考えてください)、十分なカバレッジでテストを作成することが非常に迅速に不可能になるため、統合テストを行うだけでより良い完全なテストを達成できると考えることはほとんど不可能です。

要するに、ほとんどすべてのプロジェクトで通常使用する3つのレベルの「単体テスト」です。

  • モックまたはスタブ化された依存関係で分離され、単一のコンポーネントの地獄をテストする単体テスト。理想的には、完全なカバレッジを試みます。
  • より微妙なエラーをテストする統合テスト。制限ケースと典型的な条件を示す、慎重に作成されたスポットチェック。多くの場合、完全なカバレッジは不可能であり、システムが故障する原因に焦点を当てています。
  • システムにさまざまな実際のデータを注入し、データを計測し(可能な場合)、出力を観察して仕様とビジネスルールに準拠していることを確認する仕様テスト。

依存性注入は、システムを複雑にすることなく、ユニットテスト用のコンポーネントを非常に簡単に分離できるため、これを達成するための非常に効率的な手段の1つです。あなたのビジネスシナリオはインジェクションメカニズムの使用を保証しないかもしれませんが、あなたのテストシナリオはほとんどそうです。私にとってはこれで十分です。これらを使用して、部分統合テストを介してさまざまな抽象化レベルを個別にテストすることもできます。


4

これの反対側は何ですか?単体テストが依存関係もテストしないことは本当に重要ですか?もしそうなら、なぜですか?

単位。単数を意味します。

2つのことをテストするということは、2つのことすべての機能的な依存関係があることを意味します。

3つ目を追加すると、テストの機能的な依存関係が直線的に増加します。モノ間の相互接続は、モノの数よりも速く成長します。

テスト対象のn個のアイテムには、n(n-1)/ 2の潜在的な依存関係があります。

それが大きな理由です。

シンプルさには価値があります。


2

最初に再帰を学んだことを覚えていますか?私の教授は、「xを実行するメソッドがあると仮定します」(たとえば、xのfibbonacciを解く)と言いました。「xを解くには、x-1およびx-2のメソッドを呼び出す必要があります」。同様に、依存関係をスタブアウトすることで、依存関係が存在するふりをして、現在のユニットが実行すべきことをテストできます。もちろん、依存関係を厳密にテストしていることを前提としています。

これは本質的には作業中のSRPです。テストに対しても単一の責任に焦点を合わせると、必要なメンタルジャグリングの量がなくなります。


2

(これは小さな答えです。ヒントをありがとう@TimWilliscroft。)

障害は、次の場合に簡単にローカライズできます。

  • テスト対象ユニットとその依存関係の両方が、個別にテストされます。
  • 各テストは、テストの対象となるコードに障害がある場合にのみ失敗します。

これは紙の上でうまく機能します。ただし、OPの説明に示されているように(依存関係にはバグがあります)、依存関係がテストされていない場合、障害の場所を特定するのは困難です。


なぜ依存関係はテストされないのですか?これらはおそらくレベルが低く、単体テストが簡単です。さらに、特定のコードを対象とする単体テストを使用すると、障害の場所を簡単に特定できます。
デビッドソーンリー

2

たくさんの良い答え。他にもいくつかのポイントを追加します。

単体テストでは、依存関係が存在しないときにコードをテストすることもできます。たとえば、あなた、またはあなたのチームは、まだ他のレイヤーを書いていないか、別の会社が提供するインターフェースを待っているかもしれません。

ユニットテストは、開発マシンに完全な環境(データベース、Webサーバーなど)を用意する必要がないことも意味します。私は強く、すべての開発者がいることをお勧めしません何らかの理由でそれが本番環境を模倣することができない場合には、それはREPROバグのためには、伐採しかし、このような環境を持っているなどしかし、その後、ユニットテストは、少なくとも湯にいくつかのレベルを提供しますより大きなテストシステムに入る前に、コードに自信を持ってください。


0

設計の側面について:コードをテスト可能にすることは、小さなプロジェクトでもメリットがあると思います。必ずしもGuiceのようなものを導入する必要はありません(単純なファクトリクラスでよく行われます)が、構築プロセスをプログラミングロジックから分離すると、

  • インターフェースを介してすべてのクラスの依存関係を明確に文書化する(チームに新しい人にとって非常に役立つ)
  • クラスがより明確になり、保守が容易になります(いオブジェクトグラフの作成を別のクラスにすると)
  • 疎結合(変更をはるかに容易にする)

2
方法:はい。ただし、これを行うにはコードとクラスを追加する必要があります。コードはできる限り簡潔でありながら、読み取り可能である必要があります。私見や工場を追加することは、あなたが必要とすることを証明できるか、それが提供する柔軟性を必要とする可能性が非常に高い場合を除き、過剰なエンジニアリングです。
-dsimcha

0

ええと、これらの回答に記載されている単体テストと統合テストには良い点があります!

ここで、コスト関連の実用的な見方を逃しています。それは、非常に孤立した/原子単体テスト(互いに非常に独立している可能性があり、データベース、ファイルシステムなどのような依存関係なしでそれらを並行して実行するオプションがあります)と(より高いレベルの)統合テストの利点を明確に見ると言いましたしかし...それはコスト(時間、お金など)とリスクの問題であります

私の経験から「テスト方法」を考える前に、もっと重要な他の要因(「テストするもの」など)があります...

顧客は(暗黙的に)余分な量のテストの作成と保守に料金を支払いますか?よりテスト駆動型のアプローチ(コードを書く前に最初にテストを書く)が私の環境(コード障害のリスク/コスト分析、人、設計仕様、テスト環境のセットアップ)で本当にコスト効率が良いですか?コードには常にバグがありますが、テストを実稼働環境に移行する方が費用対効果が高いでしょうか(最悪の場合!)?

また、コードの品質(標準)が何であるか、フレームワーク、IDE、設計原則などに大きく依存します。あなたとあなたのチームが従うもの、およびそれらがどの程度経験されているかです。よく書かれた、理解しやすく、十分に文書化された(理想的には自己文書化)モジュール式のコードは、逆の場合よりもバグが少ない可能性があります。したがって、広範なテストの実際の「必要性」、プレッシャー、または全体的なメンテナンス/バグ修正コスト/リスクは高くない場合があります。

私のチームの同僚が提案した極端な話をしましょう。データベース内のすべてのクラスとモックを作成するために、純粋なJava EEモデルレイヤーコードを希望する100%のカバレッジで単体テストする必要があります。または、統合テストをすべての可能な現実世界のユースケースとWeb UIワークフローの100%でカバーすることを希望するマネージャーは、ユースケースが失敗するリスクを冒したくないためです。しかし、約100万ユーロという厳しい予算があり、すべてをコーディングするかなり厳しい計画があります。潜在的なアプリのバグが人間や企業にとって非常に大きな危険ではない顧客環境。私たちのアプリは、(一部の)重要な単体テスト、統合テスト、設計されたテスト計画による主要な顧客テスト、テスト段階などで内部テストされます。一部の原子力工場や医薬品製造向けのアプリは開発していません!

私自身は、可能であればテストを作成し、コードの単体テスト中に開発しようとします。しかし、私はしばしばトップダウンのアプローチ(統合テスト)からそれを行い、重要なテスト(多くの場合モデルレイヤーで)の「アプリレイヤーカット」を行う良いポイントを見つけようとします。(多くの場合、「レイヤー」についてです)

さらに、ユニットと統合テストのコードは、時間、お金、メンテナンス、コーディングなどにマイナスの影響を与えません。クールで適用する必要がありますが、多くの開発されたテストの開始時または5年後の影響を慎重に検討してくださいコード。

ですから、信頼やコスト/リスク/利益の評価に大きく依存していると言えます。現実の世界のように、たくさんの、または100%の安全メカニズムで走り回ったり、走り回ったりしたくないようなものです。

子供はどこかに登ることができ、また登る必要があり、転倒して怪我をする可能性があります。間違った燃料を入れたため、車が動作しなくなる可能性があります(無効な入力:))。タイムボタンが3年後に機能しなくなった場合、トーストが燃えることがあります。しかし、私は高速道路で運転したり、取り外したステアリングホイールを手にしたくはありません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.