7

次の課題がありますが、良い答えが見つかりません。モッキング フレームワーク (この場合は JMock) を使用して、単体テストをデータベース コードから分離できるようにしています。データベース ロジックを含むクラスへのアクセスをモックし、DBUnit を使用してデータベース クラスを個別にテストしています。

私が抱えている問題は、ロジックが概念的に複数の場所で複製されているパターンに気付いていることです。たとえば、データベースに値が存在しないことを検出する必要があるため、その場合、メソッドから null を返すことがあります。したがって、データベースとの対話を行い、null を適切に返すデータベース アクセス クラスがあります。次に、モックからnullを受け取り、値がnullの場合に適切に動作するようにテストされるビジネスロジッククラスがあります。

将来、動作を変更する必要があり、状態がより複雑になったために null を返すことが適切でなくなったとしたら、値が存在しないことを報告するオブジェクトと、からの追加の事実を返す必要があります。データベース。

ここで、データベース クラスの動作を変更してその場合に null を返さないようにすると、ビジネス ロジック クラスは引き続き機能しているように見え、バグは QA でのみ検出されます。メソッドの使用法。

私は何かが欠けているように感じました。この概念の重複を回避するためのより良い方法が必要です。または、変更された場合に変更が反映されないという事実が単体テストに失敗するように、少なくともテストを実施する必要があります。

助言がありますか?

アップデート:

私の質問を明確にしてみましょう。コードが時間の経過とともに進化するとき、モックを介してテストされたクラスとモックが表すクラスの実際の実装との間で統合が壊れないようにする方法を考えています。

たとえば、最初に作成されたメソッドがあり、null 値を想定していない場合があったため、これは実際のオブジェクトのテストではありませんでした。その後、(モックを介してテストされた) クラスのユーザーは、特定の状況下でパラメーターとして null を渡すように拡張されました。実際のクラスがnullについてテストされていなかったため、統合が壊れました。最初にこれらのクラスを構築するとき、構築しながら両端をテストするので、これは大したことではありませんが、詳細を忘れがちな 2 か月後に設計を進化させる必要がある場合、間の相互作用をどのようにテストしますか?これらの 2 つのオブジェクトのセット (モックと実際の実装を介してテストされたもの)?

根底にある問題は重複 (DRY 原則に違反している) の 1 つであると思われます。関係は概念的なものですが、実際の重複コードはありません。

[Aaron Digulla の回答に対する 2 回目の編集後に編集]:

そうです、それはまさに私がやっていることです(DBUnitを介してテストされ、テスト中にデータベースと対話するクラスでDBとのさらなる対話があることを除いて、それは同じ考えです)。ここで、結果が異なるようにデータベースの動作を変更する必要があるとします。モックを使用したテストは、1) 誰かが覚えているか、2) 統合で壊れない限り合格し続けます。したがって、データベースのストアド プロシージャの戻り値 (たとえば) は、モックのテスト データで本質的に複製されます。重複について気になるのは、ロジックが重複していることです。これは、DRY の微妙な違反です。その通りなのかもしれませんが (結局のところ、統合テストには理由があります)、代わりに何かが足りないと感じていました。

[バウンティ開始時の編集]

アーロンとのやり取りを読むと、問題の要点に到達しますが、私が本当に探しているのは、明らかな重複を回避または管理する方法についての洞察であり、実際のクラスの動作の変化が壊れたものとしてモックと相互作用する単体テスト。明らかにそれは自動的には起こりませんが、シナリオを正しく設計する方法があるかもしれません。

[バウンティの授与に関する編集]

時間を割いて質問に答えてくれたすべての人に感謝します。勝者は、2 つのレイヤー間でデータを渡す方法について新しいことを教えてくれ、最初に答えにたどり着きました。

4

11 に答える 11

5

あなたは基本的に不可能を求めています。外部リソースの動作を変更したときに予測して通知する単体テストを求めています。新しい行動を生み出すためのテストを書かずに、どうやって彼らは知ることができますか?

あなたが説明しているのは、テストする必要のあるまったく新しい状態を追加することです-nullの結果の代わりに、データベースから出てくるオブジェクトがあります。テストスイートは、新しいランダムなオブジェクトに対して、テスト対象のオブジェクトの意図された動作がどうあるべきかをどのように知ることができますか?新しいテストを書く必要があります。

あなたがコメントしたように、モックは「誤動作」ではありません。モックは、設定したとおりに実行されます。仕様が変更されたという事実は、モックには影響しません。このシナリオの唯一の問題は、変更を実装した人が単体テストを更新するのを忘れたことです。懸念の重複が起こっているとあなたが考える理由は、実際にはあまりよくわかりません。

システムに新しい戻り結果を追加しているコーダーは、このケースを処理するための単体テストを追加する責任があります。そのコードでも、nullの結果が返される可能性がないことを100%確信している場合は、古い単体テストを削除することもできます。しかし、なぜあなたはそうしますか?単体テストは、nullの結果を受け取ったときの、テスト対象のオブジェクトの動作を正しく記述します。システムのバックエンドを、nullを返す新しいデータベースに変更するとどうなりますか?仕様がnullを返すように戻った場合はどうなりますか?オブジェクトに関する限り、外部リソースから実際に何かを取り戻すことができ、考えられるすべてのケースを適切に処理する必要があるため、テストを継続することをお勧めします。

モックの全体的な目的は、テストを実際のリソースから切り離すことです。システムにバグが発生するのを自動的に防ぐことはできません。ユニットテストがnullを受け取ったときの動作を正確に記述している場合は、すばらしいです。ただし、このテストには他の状態に関する知識がなく、外部リソースがnullを送信しなくなることを何らかの方法で通知する必要はありません。

適切で緩く結合された設計を行っている場合、システムには想像できる任意のバックエンドが含まれている可能性があります。1つの外部リソースを念頭に置いてテストを作成するべきではありません。実際のデータベースを使用する統合テストをいくつか追加して、モックレイヤーを排除した方が幸せなようです。これは、ビルドまたは正気/スモークテストを行う際に使用するのに常に優れたアイデアですが、通常、日々の開発を妨げるものです。

于 2009-05-08T17:43:03.640 に答える
4

ここで何かが欠けているわけではありません。これは、モック オブジェクトを使用した単体テストの弱点です。ユニットテストを適切なサイズのユニットに適切に分割しているようです。これは良いことです。「単体」テストでテストしすぎている人を見つけるのは、はるかに一般的です。

残念ながら、このレベルの粒度でテストすると、単体テストは共同作業するオブジェクト間の相互作用をカバーしません。これをカバーするには、いくつかの統合テストまたは機能テストが必要です。私はそれよりも良い答えを本当に知りません。

場合によっては、単体テストでモックの代わりに実際の共同作業者を使用することが実用的です。たとえば、データ アクセス オブジェクトの単体テストを行っている場合、モックの代わりに単体テストで実際のドメイン オブジェクトを使用すると、多くの場合、設定が簡単で、同じように実行できます。その逆は当てはまりません。通常、データ アクセス オブジェクトは、データベース接続、ファイル、またはネットワーク接続を必要とし、設定が非常に複雑で時間がかかります。ドメイン オブジェクトの単体テスト時に実際のデータ オブジェクトを使用すると、数マイクロ秒かかる単体テストが数百または数千ミリ秒かかる単体テストに変わります。

要約すると:

  1. 連携するオブジェクトの問題を検出するために、いくつかの統合/機能テストを記述します
  2. 必ずしも共同編集者をからかう必要はありません。最善の判断を下してください
于 2009-05-08T22:22:39.537 に答える
2

単体テストでは、メソッドの可能な結果のセットが突然少なくなった場合はわかりません。それがコードカバレッジの目的です。コードがもう実行されていないことがわかります。これにより、アプリケーション層でデッドコードが発見されます。

[編集]コメントに基づく:モックは、テスト対象のクラスをインスタンス化し、追加情報を収集できるようにする以外に何もしてはなりません。特に、テストしたい結果に影響を与えてはなりません。

[EDIT2]データベースをモックするということは、DBドライバーが機能するかどうかを気にしないことを意味します。知りたいのは、コードがDBから返されたデータを正しく解釈できるかどうかです。また、これは、実際のDBドライバーに「このSQLが表示されたら、このエラーをスローする」とは言えないため、エラー処理が正しく機能するかどうかをテストする唯一の方法です。これはモックでのみ可能です。

同意します。慣れるまでには少し時間がかかります。これが私がすることです:

  • SQLが機能するかどうかをチェックするテストがあります。各SQLは静的テストDBに対して1回実行され、返されたデータが期待どおりであることを確認します。

  • 他のすべてのテストは、事前定義された結果を返すモックDBコネクタを使用して実行されます。データベースに対してコードを実行し、どこかに主キーを記録することで、これらの結果を得るのが好きです。次に、これらの主キーを取得し、モックを使用してJavaコードをSystem.outにダンプするツールを作成します。このようにして、新しいテストケースを非常に迅速に作成でき、テストケースは「真実」を反映します。

    さらに良いことに、古いIDとツールを再度実行することで、(DBが変更されたときに)古いテストを再作成できます。

于 2009-03-13T16:09:37.090 に答える
2

データベースの抽象化では、nullを使用して「結果が見つかりません」を意味します。オブジェクト間でnullを渡すのは悪い考えであるという事実を無視して、何も見つからないときに何が起こるかをテストするときに、テストでそのnullリテラルを使用しないでください。代わりに、定数またはテストデータビルダーを使用して、テストがオブジェクト間で渡される情報のみを参照し、その情報がどのように表されるかを参照しないようにします。次に、データベースレイヤーが「結果が見つかりません」(またはテストが依存する情報)を表す方法を変更する必要がある場合、それを変更する場所はテスト内に1つだけです。

于 2009-05-12T15:11:18.640 に答える
1

これが私があなたの質問を理解する方法です:

エンティティのモックオブジェクトを使用して、JMockを使用してアプリケーションのビジネスレイヤーをテストしています。また、DBUnitを使用してDAOレイヤー(アプリとデータベース間のインターフェイス)をテストし、既知の値のセットが入力されたエンティティオブジェクトの実際のコピーを渡します。テストオブジェクトを準備するために2つの異なる方法を使用しているため、コードはDRYに違反しており、コードが変更されるとテストが現実と同期しなくなるリスクがあります。

フォロワーは言う...

まったく同じではありませんが、MartinFowlerのMocksAren'tStubsの記事を確かに思い出させます。JMockルートはモックの方法であり、「実際のオブジェクト」ルートはテストを実行するための古典的な方法であると思います。

この問題に取り組むときにできるだけ乾くための1つの方法は、古典主義者であり、モック主義者であるということです。たぶん、あなたはあなたのテストであなたのBeanオブジェクトの実際のコピーを妥協して使うことができます。

重複を避けるためのユーザーメーカー

1つのプロジェクトで行ったことは、ビジネスオブジェクトごとにMakerを作成することです。メーカーには、既知の値が入力された特定のエンティティオブジェクトのコピーを作成する静的メソッドが含まれています。次に、必要なオブジェクトの種類に関係なく、そのオブジェクトのメーカーを呼び出して、テストに使用する既知の値を含むオブジェクトのコピーを取得できます。そのオブジェクトに子オブジェクトがある場合、作成者は子の作成者を呼び出して上から下に作成し、必要なだけ完全なオブジェクトグラフを取得します。これらのメーカーオブジェクトは、すべてのテストに使用できます。DAOレイヤーをテストするときにDBに渡すだけでなく、ビジネスサービスをテストするときにサービス呼び出しに渡すこともできます。メーカーは再利用可能であるため、かなりドライなアプローチです。

ただし、JMockを使用する必要があることの1つは、サービスレイヤーをテストするときにDAOレイヤーをモックすることです。サービスがDAOに電話をかける場合は、代わりにモックが注入されていることを確認する必要があります。ただし、Makerをまったく同じように使用できます。期待値を設定するときは、関連するエンティティオブジェクトのMakerを使用して、モックされたDAOが期待される結果を返すことを確認してください。そうすれば、私たちはまだDRYに違反していません。

よく書かれたテストは、コードが変更されたときに通知します

時間の経過に伴うコード変更の問題を回避するための最後のアドバイスは、常にnull入力に対処するテストを行うことです。最初にメソッドを作成するときに、nullは受け入れられないとします。nullが使用された場合に例外がスローされることを確認するテストが必要です。後でnullが受け入れられるようになると、アプリコードが変更されて、null値が新しい方法で処理され、例外がスローされなくなる可能性があります。それが起こると、テストは失敗し始め、物事が同期していないという「注意」があります。

于 2009-05-12T21:30:36.523 に答える
1

null を返すことが外部 API の意図された部分なのか、それとも実装の詳細なのかを判断する必要があります。

単体テストでは、実装の詳細を気にする必要はありません。

それが意図した外部 API の一部である場合、変更によってクライアントが壊れる可能性があるため、当然ユニット テストも壊れるはずです。

これが NULL を返すことは、外部の POV から理解できますか? それとも、この NULL の意味に関してクライアントで直接の仮定を行うことができるため、これは便利な結果ですか? NULL は、他の意味なしに void/nix/nada/unavailable を意味する必要があります。

後でこの条件を細分化する予定がある場合は、NULL チェックを有益な例外、列挙型、または明示的に指定された bool のいずれかを返すものにラップする必要があります。

単体テストを作成する際の課題の 1 つは、最初に作成した単体テストでさえ、最終製品の完全な API を反映する必要があることです。完全な API を視覚化してから、それに対してプログラムする必要があります。

また、単体テスト コードでも、製品コードと同じ規律を維持して、重複や機能羨望などの臭いを回避する必要があります。

于 2009-05-13T11:29:52.423 に答える
0

特定のシナリオでは、コンパイル時にキャッチされるメソッドの戻り値のタイプを変更します。そうでない場合は、コードカバレッジに表示されます(Aaronが述べたように)。それでも、チェックイン後すぐに実行される自動機能テストが必要です。そうは言っても、私は自動スモークテストを行っているので、私の場合はそれを捕まえるでしょう:)。

上記を考慮しなくても、最初のシナリオでは2つの重要な要素があります。ユニットテストコードに他のコードと同じ注意を払う必要があります。つまり、それらをDRYのままにしておくのが合理的です。TDDを実行している場合、そもそもこの懸念が設計に押し付けられます。あなたがそれに興味がないなら、関係する他の反対の要因はYAGNIです、あなたはあなたのコードですべての(ありそうもない)シナリオを取得したくありません。したがって、私にとっては、次のようになります。テストで何かが不足していると表示された場合は、テストに問題がないことを再確認して、変更を続行します。それは罠なので、私のテストでシナリオを実行しないようにしています。

于 2009-03-13T16:23:55.870 に答える
0

質問を正しく理解できれば、モデルを使用するビジネス オブジェクトがあります。BO とモデル間の相互作用のテスト (テスト A) と、モデルとデータベース間の相互作用をテストする別のテスト (テスト B) があります。テスト B はオブジェクトを返すように変更されますが、テスト A のモデルがモックされているため、その変更はテスト A に影響しません。

テスト B が変更されたときにテスト A を失敗させる唯一の方法は、テスト A のモデルをモックして、2 つを 1 つのテストに結合することではありません。異なるフレームワークを使用します)。

テストを作成するときにこの依存関係について知っている場合、許容できる解決策は、各テストに依存関係と、一方が変更された場合に他方を変更する必要があることを説明するコメントを残すことだと思います。とにかくリファクタリングするときはテスト B を変更する必要があります。現在のテストは、変更を加えるとすぐに失敗します。

于 2009-05-13T14:42:24.247 に答える
-1

あなたの質問は非常に紛らわしく、テキストの量は正確には役に立ちません。

しかし、契約を変更しない変更がモックの動作に影響を与えることを望んでいるという点で、私が簡単に読んで抽出できる意味はほとんど意味がありません。

モッキングは、システムの特定の部分のテストに集中するためのイネーブラーです。モックされた部分は常に指定された方法で動作し、テストは特定のロジックのテストに集中できます。したがって、無関係なロジック、レイテンシの問題、予期しないデータなどの影響を受けることはありません.

おそらく、モック化された機能を別のコンテキストでチェックする別の数のテストがあるでしょう。

要点は、モックされたインターフェースとその実際の実装の間に接続がまったく存在してはならないということです。コントラクトを嘲笑し、独自の実装を提供しているため、意味がありません。

于 2009-05-08T14:03:22.357 に答える
-2

あなたの問題はリスコフ置換原則に違反していると思います:

サブタイプは、その基本タイプに代入可能でなければなりません

理想的には、抽象化に依存するクラスが必要です。「機能するには、このパラメーターを受け取り、この結果を返すこのメソッドの実装が必要であり、間違ったことをすると、この例外がスローされる」という抽象化。これらはすべて、コンパイル時間の制約またはコメントによって、依存するインターフェイスで定義されます。

技術的には、抽象化に依存しているように見えるかもしれませんが、あなたが言うシナリオでは、実際には抽象化に依存しているのではなく、実際には実装に依存しています。「このメソッドが動作を変更すると、ユーザーは壊れてしまい、私のテストは決して知りません」とあなたは言います。ユニットテストレベルでは、あなたは正しいです。しかし、コントラクト レベルでは、このように動作を変更することは間違っています。メソッドを変更することにより、メソッドとその呼び出し元との間の契約に明らかに違反するためです。

なぜメソッドを変更するのですか?そのメソッドの呼び出し元が別の動作を必要とすることは明らかです。したがって、最初に行う必要があるのは、メソッド自体を変更することではなく、クライアントが依存する抽象化またはコントラクトを変更することです。彼らは最初に変更し、新しいコントラクトで作業を開始する必要があります。したがって、インターフェイスを変更し、必要に応じてインターフェイスのユーザーを変更します。これには、テストの更新が含まれます。最後に、クライアントに渡す実際の実装を変更します。このようにして、あなたが話しているエラーは発生しません。

そう、

class NeedsWork(IWorker b) { DoSth() { b.Work() }; }
...
AppBuilder() { INeedWork GetA() { return new NeedsWork(new Worker()); } }
  1. NeedsWork の新しいニーズを反映するように IWorker を変更します。
  2. DoSth を変更して、新しいニーズを満たす新しい抽象化で動作するようにします。
  3. NeedsWork をテストして、新しい動作で動作することを確認します。
  4. IWorker に提供するすべての実装 (このシナリオでは Worker) を変更します (これを最初に実行しようとしています)。
  5. Worker をテストして、新しい期待に応えます。

恐ろしいように見えますが、実際には、これは小さな変更では些細なことであり、実際にはそうでなければならないので、大きな変更では苦痛です.

于 2009-05-08T21:51:10.487 に答える