単体テストでは独自のメソッドを使用すべきではありませんか?


83

今日、私は「JUnitの基本」ビデオを見ていました。著者は、プログラムで特定のメソッドをテストするとき、プロセスで他のメソッドを使用しないでくださいと言いました。

具体的には、引数に名前と姓を使用するレコード作成メソッドをテストし、それらを使用して特定のテーブルにレコードを作成することについて話していました。しかし、彼は、このメソッドをテストする過程で、他のDAOメソッドを使用してデータベースにクエリを行って最終結果を確認するべきではないと主張しました(レコードが実際に正しいデータで作成されたことを確認するため)。彼は、そのために、データベースを照会して結果を確認するための追加のJDBCコードを作成する必要があると主張しました。

私は彼の主張の精神を理解していると思います:あるメソッドのテストケースが他のメソッド(この場合はDAOメソッド)の正確さに依存することは望ましくなく、これはあなた自身の検証を(再び)書くことによって達成されます/ supportingコード(より具体的で焦点が合っている必要があるため、よりシンプルなコード)。

それにもかかわらず、私の頭の中の声は、コードの重複、不必要な追加作業などの議論で抗議し始めました。他の方法をテストしながら、それらの方法の一部を使用しても大丈夫ですか?それらの1つが想定されていることを実行していない場合、独自のテストケースは失敗します。それを修正し、テストバッテリを再度実行できます。コードの複製(複製コードがいくぶん単純であったとしても)や無駄な努力は必要ありません。

私が書いたいくつかの最近のExcel - VBAアプリケーション(VBA用Rubberduckのおかげで適切にユニットテストされた)のため、私はこれについて強い気持ちがあります。

これについての洞察を共有していただけますか?


79
データベースを含む単体テストを見るのは少し奇妙です
リチャードティングル

4
IMO他のクラスを十分に高速なIFFと呼ぶことは問題ありません。ディスクまたはネットワーク経由で送信する必要があるものはすべてモックします。昔ながらのIMOクラスをm笑する意味はありません。
ラバーダック

2
このビデオへのリンクがありますか?
candied_orange

17
VBAのRubberduckのおかげで適切に単体テストが行​​われました」タイプミスを修正し、「RubberDuck」から「Rubberduck」に編集しますが、スパマーがそれを実行し、rubberduckvba.comへのリンクを追加するように感じます(ドメイン名を所有し、プロジェクトを共同所有しています@RubberDuck)-そのため、代わりにここでコメントします。とにかく、過去2年間の大部分の私の眠れない夜のほとんどを担当していたツールを実際に使用している人々を見るのは素晴らしいです!=)
マチューギンドン

4
@ Mat'sMugとRubberDuck私は、Rubberduckでやっていることをとても気に入っています。続けてください。確かに、そのおかげで私の生活は楽になりました(たまたま、Excel VBAで小さなプログラムやプロトタイプをたくさんやっています)。ところで、私の投稿でのラバーダックの言及は、私がラバーダック自身に親切になろうとしているだけであり、それはここ PEで私に親切でした。:)
carlossierra

回答:


186

彼の主張の精神は確かに正しい。単体テストのポイントは、コードを分離し、依存関係のないコードをテストすることです。これにより、エラーが発生した場合にエラーのある動作をすばやく認識できます。

そうは言っても、単体テストはツールであり、目的を果たすためのものであり、祈る祭壇ではありません。時には、依存関係を十分に確実に機能させるために依存関係を残すことを意味し、それらをモックする必要はありません。時には、実際には統合テストではないにしても、一部のユニットテストが実際にかなり近いことを意味します。

最終的には評価されていません。重要なのは、テスト対象のソフトウェアの最終製品ですが、ルールを曲げて、トレードオフに値するタイミングを決定するときに注意する必要があります。


117
実用主義のための完全な。
ロバートハーベイ

6
私は、それが依存性を切り抜ける問題である速度であるので、それほど信頼性ではないことを付け加えます。しかし、孤立したマントラのテストは非常に説得力があるため、この議論はしばしば見落とされます。
マイケルデュラント

4
「...単体テストはツールであり、目的を果たすためのものであり、祈るべき祭壇ではありません。」-これ!
ウェインコンラッド

15
「祈るべき祭壇ではない」-怒っているTDDコーチがやってくる!
デン

5
@ jpmc26:さらに悪いことに、すべてのユニットテストが合格しないようにする必要があります。これは、ダウンしているWebサービスを正しく処理したためです。スタブを作成したら、「アップ」か「ダウン」かを選択し、両方をテストします。
スティーブジェソップ

36

これは用語に帰着すると思います。多くの人にとって、「ユニットテスト」は非常に特殊なものであり、定義上、テスト対象のユニット(メソッド、関数など)以外のコードに依存する合否条件を持つことはできません。これには、データベースとの相互作用が含まれます。

他の人にとっては、「ユニットテスト」という用語ははるかに緩やかであり、アプリケーションの統合部分をテストするテストコードを含む、あらゆる種類の自動テストを包含しています。

テストの純粋主義者(私がその用語を使用する場合)は、テストが複数の純粋なコード単位に依存することに基づいて、単体テストと区別される統合テストと呼ぶ場合があります。

「ユニットテスト」という用語の緩いバージョンを使用しており、実際には「統合テスト」を参照していると思われます。

これらの定義を使用すると、「ユニットテスト」でテスト対象のユニットの外部のコードに依存しないでください。ただし、「統合テスト」では、特定のコードを実行し、データベースで合格基準を検査するテストを作成することは完全に合理的です。

統合テストと単体テストのどちらに依存するか、または両方に依存するかどうかは、議論のはるかに大きなトピックです。


あなたはあなたの疑いに正しいです。私は用語を誤用しており、各タイプのテストをより適切に処理する方法がわかりました。ありがとう!
carlossierra

11
「他の人にとっては、「ユニットテスト」という用語ははるかに緩い」-他のこととは別に、いわゆる「ユニットテストフレームワーク」は、高レベルのテストを整理して実行するための本当に便利な方法です;-)
Steve Jessop

1
意味を含む「純粋な」対「ゆるい」区別ではなく、ドメインの観点から「ユニット」や「依存関係」などを見る人(「認証」など)の違いを区別することをお勧めしますユニットとして、「データベース」は依存関係などとしてカウントされます)対プログラミング言語の観点からそれらを見る人(例えば、メソッドはユニット、クラスは依存関係)。「テスト対象のユニット」などのフレーズが意味するものを定義すると、議論(「あなたは間違っています!」)を議論に変えることができます(「これは適切な粒度ですか?」)。
ウォーボ

1
@EricKingもちろん、最初のカテゴリの大多数の人にとって、データベースユニットテストに関与させるという考えは、DAOを使用するかデータベースに直接アクセスするかにかかわらず、嫌悪感です。
ペリアタブレアッタ16

1
@AmaniKilumanga質問は基本的に「誰かがユニットテストでこれを行うべきではないが、ユニットテストでそれを行うべきだと言っています。それでいいと思います。」そして、私の答えは「はい、しかしほとんどの人はそれを単体テストではなく統合テストと呼ぶでしょう」でした。あなたの質問に関しては、「統合テストで生産コードのメソッドを使用できますか?」の意味がわかりません。
エリックキング

7

答えはイエスとノーです...

独立して実行する独自のテストは、低レベルのコードが適切に機能していることを確認できるため、絶対に必要です。しかし、より大きなライブラリのコーディングでは、ユニットを横断するテストを必要とするコードの領域も見つかります。

これらのユニット間テストは、コードカバレッジおよびエンドツーエンドの機能をテストする場合に適していますが、次のようないくつかの欠点に注意する必要があります。

  • 何が壊れているのかを確認するための独立したテストがなければ、失敗した「クロスユニット」テストでは、コードの何が問題なのかを判断するために追加のトラブルシューティングが必要になります
  • ユニット間テストに頼りすぎると、SOLID Object-Orientedコードを記述する際に常に必要である契約の考え方から抜け出すことができます。通常、ユニットは1つの基本アクションのみを実行する必要があるため、分離テストは理にかなっています。
  • エンドツーエンドのテストが望ましいが、テストでデータベースへの書き込みが必要な場合、または運用環境で発生させたくないアクションを実行する必要がある場合は危険です。これは、Mockitoのようなモックフレームワークが非常に人気がある理由の1つです。これは、実際に行うべきでないものを変更することなく、オブジェクトを偽造し、エンドツーエンドのテストを模倣できるためです。

1日の終わりには、両方の機能を使いたいと思います。低レベルの機能をトラップする多くのテストと、エンドツーエンドの機能をテストするいくつかのテストが必要です。そもそもテストを書いている理由に注目してください。彼らはあなたのコードがあなたがそれを期待する方法で実行しているという自信をあなたに与えます。それを実現するために必要なことは何でも結構です。

悲しいが本当の事実は、自動化されたテストを使用している場合は、すでに多くの開発者に依存しているということです。開発チームが厳しい納期に直面している場合、優れたテストプラクティスを最初にスライドさせます。したがって、あなたがあなたの銃にこだわり、あなたのコードがどのように実行されるべきかの有意義な反映であるユニットテストを書いている限り、私はあなたのテストがどれだけ「純粋」であるかについて取りつかれません。


4

他のオブジェクトメソッドを使用して条件をテストする場合の問題の1つは、互いにキャンセルするエラーを見逃すことです。さらに重要なことは、困難なテスト実装の痛みを逃しており、その痛みが基礎となるコードについて何かを教えていることです。痛みを伴うテストは、後で痛みを伴うメンテナンスを意味します。

ユニットを小さくし、クラスを分割し、リファクタリングし、オブジェクトの残りを再実装せずにテストしやすくなるまで再設計します。コードやテストをこれ以上単純化できないと思われる場合は、助けを求めてください。常に運がよさそうな同僚のことを考えてみてください。そして、きれいにテストしやすい課題を取得してください。それは運ではないので、彼または彼女に助けを求めてください。


1
ここでのキーはユニットです。他の手続きやプロセスを呼び出す必要がある場合、私にとってはユニットではありません。複数の呼び出しが行われ、複数のステップを検証する必要がある場合、それがユニットよりも大きなコードの断片である場合、単一のロジックをカバーする小さな断片に分割します。通常、DB呼び出しもモックされるか、実際のDBにアクセスし、テスト後にデータを元に戻す必要があるユニットテストでメモリ内DBが使用されます。
dlb

1

テストされている機能の単位が「永続的に取得可能なデータである」場合、実際のデータベースに保存し、データベースへの参照を保持しているオブジェクトを破棄してから、コードを呼び出して戻るオブジェクト。

データベースにレコードが作成されているかどうかをテストすることは、システムの他の部分に公開されている機能のユニットをテストするのではなく、ストレージメカニズムの実装の詳細に関係しているようです。

模擬データベースを使用してパフォーマンスを向上させるためにテストをショートカットすることもできますが、そのようなテストに合格し、実際にはデータベースに何も保存しないシステムとの契約を終了する契約者がいましたシステムのリブート間。

ユニットテストの「ユニット」が「コードのユニット」を表すのか、「機能のユニット」を表すのかを議論することができます。これは有用な区別がありません-私が心に留めておくべき質問は、「テストはシステムがビジネス価値を提供するかどうかについて何かを教えてくれますか?」と「実装が変更された場合、テストは壊れやすいですか?」説明したようなテストは、システムをTDDするときに役立ちます-「データベースレコードからオブジェクトを取得する」をまだ作成していないため、機能の完全なユニットをテストできません-実装の変更に対して脆弱なので、すべての操作がテスト可能になったら削除してください。


1

精神は正しいです。

理想的には、ユニットテストでは、ユニット(個々のメソッドまたは小規模クラス)をテストします。

理想的には、データベースシステム全体をスタブアウトします。つまり、偽の環境でメソッドを実行し、正しい順序で正しいDB APIを呼び出すことを確認するだけです。明示的に、独自のメソッドの1つをテストするときに、積極的にDBをテストする必要はありません。

利点はたくさんあります。何よりも、正しいDB環境をセットアップして後でロールバックすることを気にする必要がないため、テストは目がくらむほど速くなります。

もちろん、これは、最初からこれを行わないソフトウェアプロジェクトでは、高い目標です。しかし、それは精神のあるユニットテスト。

機能テスト、動作テストなど、このアプローチとは異なる他のテストがあることに注意してください-「テスト」と「単体テスト」を混同しないでください。


「正しいDB APIを正しい順序で呼び出すことを確認してください」-そうです。それが判明するまで、ドキュメントを読み間違え、実際に使用していた正しい順序は完全に異なります。制御できないシステムをモックしないでください。
Joker_vD

4
@Joker_vDそれが統合テストの目的です。単体テストでは、外部システムは絶対にモックする必要があります。
ベンアーロンソン

1

データベースとやり取りするビジネスコードの単体テストで直接データベースアクセスを使用しないことは、少なくとも1つの非常に重要な理由があります。データベースの実装を変更する場合、それらの単体テストをすべて書き換える必要があります。列の名前を変更します。コードについては、データマッパー定義の1行を変更するだけです。ただし、テスト中にデータマッパーを使用しない場合は、この列を参照するすべての単体テストも変更する必要があります。これは、特に検索と置換の影響を受けにくいより複雑な変更の場合、並外れた量の作業になる可能性があります。

また、データベースと直接やり取りするのではなく、データマッパーを抽象メカニズムとして使用すると、データベースへの依存関係を完全削除することが容易になります。これは現在は関係ないかもしれませんが、数千のユニットテストとそれらすべてデータベースにアクセスしている場合、テストスイートが数分で実行されてから数秒で削除されるため、簡単にリファクタリングして依存関係を削除できることに感謝します。これは生産性に大きなメリットをもたらします。

あなたの質問は、あなたが正しい線に沿って考えていることを示しています。ご想像のとおり、単体テストもコードです。保守しやすく、将来の変更に簡単に適応できるようにすることは、他のコードベースと同様に重要です。それらを読みやすくし、重複を排除するように努めてください。そうすれば、より優れたテストスイートが得られます。優れたテストスイートは、使用されるものです。また、使用されるテストスイートはエラーを見つけるのに役立ちますが、使用されないテストスイートは価値がありません。


1

単体テストと統合テストについて学ぶときに学んだ最高の教訓は、メソッドをテストするのではなく、動作をテストすることです。言い換えれば、このオブジェクトは何をしますか?

そのように見ると、データを永続化するメソッドとそれを読み戻す別のメソッドがテスト可能になります。もちろん、メソッドを具体的にテストする場合は、次のような各メソッドを個別にテストすることになります。

@Test
public void canSaveData() {
    writeDataToDatabase();
    // what can you assert - the only expectation you can have here is that an exception was not thrown.
}

@Test
public void canReadData() {
    // how do I even get data in there to read if I cannot call the method which writes it?
}

この問題は、テスト方法の観点から発生します。メソッドをテストしないでください。テスト動作。WidgetDaoクラスの動作は何ですか?ウィジェットを保持します。OK、ウィジェットが持続することをどのように確認しますか?さて、永続性の定義は何ですか?それはあなたがそれを書くとき、あなたは再びそれを読み返すことができることを意味します。したがって、読み取りと書き込みの両方がテストになり、私の意見では、より意味のあるテストになります。

@Test
public void widgetsCanBeStored() {
    Widget widget = new Widget();
    widget.setXXX.....
    // blah

    widgetDao.storeWidget(widget);
    Widget stored = widgetDao.getWidget(widget.getWidgetId());
    assertEquals(widget, stored);
}

それは論理的で、まとまりがあり、信頼でき、私の意見では、意味のあるテストです。

他の答えは、分離がいかに重要であるか、実際的な議論と現実的な議論、および単体テストがデータベースを照会できるかどうかに焦点を当てています。しかし、それらは本当に質問に答えません。

何かを保存できるかどうかをテストするには、保存してから読み返す必要があります。読み取りが許可されていない場合、何かが保存されているかどうかをテストすることはできません。データの取得とは別にデータの保存をテストしないでください。あなたは何も言わないテストになってしまいます。


非常に洞察に富んだアプローチであり、あなたが言うように、あなたは私が持っていた中核的な疑問の一つに本当に当たったと思います。ありがとう!
carlossierra

0

キャッチしたい失敗の例の1つは、テスト対象のオブジェクトがキャッシングレイヤーを使用しているが、必要に応じてデータを永続化できないことです。次に、オブジェクトにクエリを実行すると、「はい、新しい名前とアドレスがあります」と表示されますが、テストは実際に実行されていないため失敗します。

あるいは(単一責任違反を見落としている)、文字列のUTF-8エンコードバージョンをバイト指向フィールドに永続化する必要があるが、実際にはShift JISを永続化すると仮定します。他のコンポーネントがデータベースを読み取り、UTF-8が表示されることを期待しているため、要件です。次に、このオブジェクトを往復すると、正しい名前と住所が報告されます。これは、Shift JISから変換されるためです。ただし、テストではエラーは検出されません。後の統合テストで検出されることを期待していますが、単体テストのポイントは、問題を早期に発見し、責任のあるコンポーネントを正確に知ることです。

それらの1つが想定されていることを実行していない場合、独自のテストケースは失敗し、修正してテストバッテリを再度実行できます。

これを想定することはできません。注意しないと相互に依存する一連のテストを書くからです。「保存しますか?」testはテスト対象のsaveメソッドを呼び出し、次にloadメソッドを呼び出して保存されたことを確認します。「ロードしますか?」testはsaveメソッドを呼び出してテストフィクスチャをセットアップし、次にテスト中のloadメソッドを呼び出して結果を確認します。どちらのテストも、テストしていないメソッドの正確性に依存しています。つまり、どちらもテスト対象のメソッドの正確性を実際にテストしていません。

ここに問題があるという手がかりは、異なるユニットをテストしているはずの2つのテストが実際に同じことを行うということです。どちらもセッターに続いてゲッターを呼び出し、結果が元の値であることを確認します。しかし、セッター/ゲッターのペアが連携して動作するのではなく、セッターがデータを保持することをテストしたいと考えました。何かが間違っていることを知っているので、テストを修正して修正する必要があります。

コードがユニットテスト用に適切に設計されている場合は、テスト対象のメソッドによってデータが実際に正しく永続化されているかどうかをテストできる少なくとも2つの方法があります。

  • データベースインターフェースをモックし、適切な関数が呼び出されたことをモックに期待値とともに記録させます。これは、メソッドが想定されていることを実行することをテストするもので、従来の単体テストです。

  • まったく同じ意図で実際のデータベースに渡し、データが正しく永続化されているかどうかを記録します。しかし、「うん、正しいデータを取得しました」とだけ言うモック関数を使用するのではなく、テストはデータベースから直接読み取り、それが正しいことを確認します。これは可能限り純粋なテストではない可能性があります。データベースエンジン全体は、見栄えの良いモックを作成するために使用する非常に大きなものであり、何かが間違っていてもテストに合格する微妙な部分を見落とす可能性が高いためです(たとえば、コミットされていないトランザクションが表示される可能性があるため、書き込みに使用されたのと同じデータベース接続を使用して読み取りを行うべきではありません)。しかし、それは正しいことをテストし、少なくともあなたはそれを正確に知っている モックコードを記述することなく、データベースインターフェイス全体を実装します。

したがって、JDBCを使用してテストデータベースからデータを読み取るか、データベースをモックするかは、テスト実装の単なる詳細です。いずれにせよ、同じクラスの他の間違ったメソッドと共謀して何かが間違っている場合でも正しく見えるようにする場合、分離することでユニットをテストすることができます。そのため、メソッドをテストしているコンポーネントを信頼する以外に、正しいデータが保持されていることを確認するための便利な手段を使用したいと思います。

コードが単体テスト用に適切に設計されていない場合、テストするメソッドを持つオブジェクトがデータベースを挿入された依存関係として受け入れない可能性があるため、選択の余地がない場合があります。その場合、テスト対象のユニットを分離する最良の方法に関する議論は、テスト中のユニットの分離にどれだけ近いかについての議論に変わります。ただし、結論は同じです。障害のあるユニット間の陰謀を回避できる場合は、利用可能な時間や、コード内の障害を見つけるのに効果的だと思われる他の条件に応じて行います。


0

これは私がそれをやりたい方法です。これは製品の結果には影響せず、生産方法にのみ影響するため、個人的な選択にすぎません。

これは、テストするアプリケーションなしでデータベースにモック値を追加できるかどうかに依存します。

2つのテストがあるとします。A-データベースの読み取りB-データベースへの挿入(Aに依存)

Aが合格の場合、Bの結果は依存関係ではなくBでテストされた部分に依存することは確実です。Aが失敗した場合、Bのフォールスネガティブが発生する可能性があります。エラーはBまたはAにある可能性があります。Aが成功を返すまで確実に知ることはできません。

アプリケーションの背後にあるDB構造を知ることができない(または変更される可能性がある)ため、ユニットテストでクエリを記述しようとはしません。コード自体が原因で、テストケースの意味が失敗する可能性があります。あなたがテスト用のテストを書くが、誰がテスト用のテストをテストするのでなければ...


-1

最終的には、テストするものになります。

クラスの提供されたインターフェースをテストするだけで十分な場合があります(たとえば、レコードを作成して保存し、後で「再起動」後に取得することができます)。この場合、テストに提供されたメソッドを使用することは問題ありません(通常は良い考えです)。これにより、たとえば、ストレージメカニズム(異なるデータベース、テスト用のインメモリデータベースなど)を変更する場合に、テストを再利用できます。

これは、クラスがその契約(この場合はレコードの作成、保存、取得)を順守していることだけを気にしていることを意味し、これを達成する方法ではないことに注意してください。ユニットテストを(クラスではなく、インターフェイスに対して)スマートに作成すると、インターフェイスのコントラクトを遵守する必要があるため、さまざまな実装をテストできます。

一方、場合によっては、データがどのように永続化されるかを実際に確認する必要があります(おそらく、他のプロセスは、データベース内の正しい形式のデータに依存していますか?)。これで、クラスから正しいレコードを取得するだけではなく、データベースに正しく保存されていることを確認する必要があります。そして、それを行う最も直接的な方法は、データベース自体を照会することです。

クラスのコントラクト(作成、保存、取得)を順守するだけでなく、これをどのように実現するか(永続データは別のインターフェイスに順守する必要があるため)を気にする必要はありません。ただし、テストはクラスインターフェイスストレージ形式の両方に依存しているため、完全に再利用することは困難です。(これらの種類のテストを「統合テスト」と呼ぶ場合があります。)


@downvoters:私の答えが欠けていると思われる側面を改善できるように、理由を教えていただけますか?
hoffmale
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.