web-dev-qa-db-ja.com

データベース駆動型アプリケーションの単体テストに最適な戦略は何ですか?

私は、さまざまな複雑さのデータベースによってバックエンドで駆動される多くのWebアプリケーションを使用しています。通常、ビジネスおよびプレゼンテーションロジックとは別の ORM レイヤーがあります。これにより、ビジネスロジックの単体テストが非常に簡単になります。物事は個別のモジュールに実装でき、テストに必要なデータはオブジェクトのモッキングを通じて偽造できます。

しかし、ORMとデータベース自体のテストには、常に問題と妥協が伴います。

長年にわたり、私はいくつかの戦略を試しましたが、どれも私を完全に満足させるものではありませんでした。

  • テストデータベースに既知のデータをロードします。 ORMに対してテストを実行し、正しいデータが返されることを確認します。ここでの欠点は、テストデータベースがアプリケーションデータベースのスキーマの変更に対応しなければならず、同期が取れなくなる可能性があることです。また、人工データに依存しており、愚かなユーザー入力のために発生するバグを公開しない場合があります。最後に、テストデータベースが小さい場合、インデックスがないなどの非効率性は明らかになりません。 (OK、最後の1つは実際に単体テストを使用するべきものではありませんが、害はありません。)

  • 本番データベースのコピーをロードし、それに対してテストします。ここでの問題は、いつでも本番DBに何があるかわからない場合があることです。データが時間とともに変化する場合、テストを書き直す必要がある場合があります。

一部の人々は、これらの戦略の両方が特定のデータに依存しており、単体テストでは機能のみをテストする必要があることを指摘しています。そのために、私は提案を見ました:

  • モックデータベースサーバーを使用し、特定のメソッド呼び出しに応答してORMが正しいクエリを送信していることのみを確認します。

データベース駆動型アプリケーションがある場合、そのテストにどの戦略を使用しましたか?あなたに最適なものは何ですか?

321
friedo

私は実際にあなたの最初のアプローチをかなり成功して使用しましたが、あなたの問題のいくつかを解決すると思う少し異なる方法で:

  1. チェックアウト後に誰でも現在のデータベーススキーマを作成できるように、スキーマとスクリプトをソース管理で作成するためにスキーマ全体を保持します。さらに、ビルドプロセスの一部によって読み込まれるデータファイルにサンプルデータを保持します。エラーの原因となるデータを発見したら、サンプルデータに追加して、エラーが再発生しないことを確認します。

  2. 継続的統合サーバーを使用して、データベーススキーマを構築し、サンプルデータをロードし、テストを実行します。これが、テストデータベースの同期を維持する方法です(テスト実行ごとに再構築します)。これには、CIサーバーが独自の専用データベースインスタンスのアクセス権と所有権を持っている必要がありますが、dbスキーマを1日に3回構築すると、おそらく配信の直前まで見つからなかったエラーを劇的に見つけるのに役立ちました(遅くない場合) )。すべてのコミットの前にスキーマを再構築するとは言えません。誰か?このアプローチを使用する必要はありません(多分そうすべきですが、誰かが忘れても大したことではありません)。

  3. 私のグループでは、ユーザー入力はアプリケーションレベル(dbではない)で行われるため、これは標準の単体テストでテストされます。

本番データベースのコピーのロード:
これは私の最後の仕事で使用されたアプローチでした。それはいくつかの問題の大きな痛みの原因でした:

  1. コピーは製品版から古くなってしまいます
  2. コピーのスキーマに変更が加えられ、本番システムに反映されません。この時点で、スキーマは異なります。楽しくない。

データベースサーバーのモック:
これも現在の仕事で行っています。コミットするたびに、モックdbアクセサーが挿入されたアプリケーションコードに対してユニットテストを実行します。その後、1日に3回、上記の完全なdbビルドを実行します。両方のアプローチをお勧めします。

146
Mark Roddy

私は、次の理由により、インメモリDB(HSQLDBまたはDerby)に対して常にテストを実行しています。

  • テストDBに保持するデータとその理由を考えることができます。実稼働DBをテストシステムに持ち込むだけで、「自分が何をしているのか、なぜなのかわからず、何かが壊れてもそれは私ではない!!」 ;)
  • データベースを新しい場所で少しの労力で再作成できるようにします(たとえば、本番からバグを複製する必要がある場合)
  • DDLファイルの品質に非常に役立ちます。

テストが開始されると、インメモリDBに新しいデータがロードされ、ほとんどのテストの後、ROLLBACKを呼び出して安定性を維持します。 ALWAYSテストDBのデータを安定させます!データが常に変化する場合、テストすることはできません。

データは、SQL、テンプレートDB、またはダンプ/バックアップからロードされます。 VCSに配置できるため、読み取り可能な形式のダンプが好きです。それでもうまくいかない場合は、CSVファイルまたはXMLを使用します。膨大な量のデータをロードする必要がある場合は...しません。膨大な量のデータを読み込む必要はありません:)単体テスト用ではありません。パフォーマンステストは別の問題であり、異なるルールが適用されます。

55
Aaron Digulla

私は長い間この質問をしてきましたが、そのための特効薬はないと思います。

現在私がしていることは、DAOオブジェクトをモックし、データベースに存在する可能性のあるデータの興味深いケースを表すオブジェクトの優れたコレクションのメモリ表現を保持することです。

私がこのアプローチで見ている主な問題は、DAOレイヤーと相互作用するコードのみをカバーしているが、DAO自体をテストすることはないということです。私の経験では、そのレイヤーでも多くのエラーが発生します。また、データベースに対して実行するいくつかのユニットテストを保持しています(TDDを使用するか、ローカルでクイックテストを行うため)。これらのテストは、その目的のためにデータベースを保持しないため、継続的統合サーバーで実行されることはありません。 CIサーバーで実行されるテストは自己完結型である必要があると思います。

私が非常に興味深いと思う別のアプローチは、少し時間がかかるので常に価値があるわけではありませんが、単体テスト内で実行するだけの組み込みデータベースで本番用に使用するのと同じスキーマを作成することです。

このアプローチがカバレッジを改善することは間違いありませんが、現在のDBMSと組み込みの置換の両方で動作させるためには、ANSI SQLに可能な限り近づけなければならないため、いくつかの欠点があります。

あなたのコードにより関連すると思われるものが何であれ、 DbUnit のように、それを簡単にするプロジェクトがいくつかあります。

14
kolrie

何らかの方法でデータベースをモックできるツールがある場合でも(たとえば jOOQ 's MockConnectionこの回答 で見られます-免責事項、私はjOOQのベンダーで働いています)、私はnotにアドバイスします複雑なクエリで大規模なデータベースをモックします。

ORMの統合テストだけを行う場合でも、ORMがデータベースに対して非常に複雑な一連のクエリを発行することに注意してください。

  • 構文
  • 複雑
  • 注文(!)

実際にモック内に小さなデータベースを構築して、送信されたSQLステートメントを解釈しない限り、すべてをモックして適切なダミーデータを生成することは非常に困難です。そうは言っても、よく知られたデータで簡単にリセットできる、よく知られた統合テストデータベースを使用してください。このデータベースに対して統合テストを実行できます。

12
Lukas Eder

最初のものを使用します(テストデータベースに対してコードを実行します)。このアプローチで提起する唯一の実質的な問題は、スキーマが同期しなくなる可能性です。これは、データベースにバージョン番号を保持し、各バージョンの増分の変更を適用するスクリプトを介してすべてのスキーマを変更することで対処します。

また、最初にテスト環境に対してすべての変更(データベーススキーマへの変更を含む)を行うため、結果は逆になります。すべてのテストに合格した後、運用ホストにスキーマの更新を適用します。また、開発システムにテストデータベースとアプリケーションデータベースを別々にペアにしておくと、実際の運用環境に触れる前に、dbのアップグレードが適切に機能することを確認できます。

5
Dave Sherohman

私は最初のアプローチを使用していますが、あなたが言及した問題に対処することができる少し異なっています。

DAOのテストを実行するために必要なものはすべてソース管理にあります。 DBを作成するためのスキーマとスクリプトが含まれています(これには、dockerが非常に適しています)。組み込みDBを使用できる場合-速度のために使用します。

説明されている他のアプローチとの重要な違いは、テストに必要なデータがSQLスクリプトまたはXMLファイルから読み込まれないことです。すべて(事実上一定である一部の辞書データを除く)は、ユーティリティ関数/クラスを使用してアプリケーションによって作成されます。

主な目的は、テストで使用されるデータを作成することです

  1. テストに非常に近い
  2. 明示的(データにSQLファイルを使用すると、どのテストでどのデータが使用されているかを確認するのが非常に困難になります)
  3. 無関係な変更からテストを分離します。

基本的に、これらのユーティリティは、テスト自体にテストに不可欠なものだけを宣言的に指定し、無関係なものを省略できることを意味します。

実際の意味を理解するために、Commentによって書かれたPostsからAuthorssで動作するDAOのテストを検討してください。このようなDAOのCRUD操作をテストするには、DBにいくつかのデータを作成する必要があります。テストは次のようになります。

@Test
public void savedCommentCanBeRead() {
    // Builder is needed to declaratively specify the entity with all attributes relevant
    // for this specific test
    // Missing attributes are generated with reasonable values
    // factory's responsibility is to create entity (and all entities required by it
    //  in our example Author) in the DB
    Post post = factory.create(PostBuilder.post());

    Comment comment = CommentBuilder.comment().forPost(post).build();

    sut.save(comment);

    Comment savedComment = sut.get(comment.getId());

    // this checks fields that are directly stored
    assertThat(saveComment, fieldwiseEqualTo(comment));
    // if there are some fields that are generated during save check them separately
    assertThat(saveComment.getGeneratedField(), equalTo(expectedValue));        
}

これには、テストデータを含むSQLスクリプトまたはXMLファイルに比べていくつかの利点があります。

  1. コードのメンテナンスがはるかに簡単です(たとえば、Authorなどの多くのテストで参照されるエンティティに必須の列を追加すると、多くのファイル/レコードを変更する必要はなく、ビルダーやファクトリーを変更するだけで済みます)
  2. 特定のテストに必要なデータは、他のファイルではなく、テスト自体に記述されています。この近接性は、テストの理解性にとって非常に重要です。

ロールバックとコミット

テストは実行時にコミットする方が便利だと思います。まず、コミットが発生しない場合、一部の効果(たとえば、DEFERRED CONSTRAINTS)をチェックできません。次に、テストが失敗した場合、データはロールバックによって元に戻されないため、DBで検査できます。

原因には、テストが破損したデータを生成する可能性があり、これが他のテストの失敗につながるという欠点があります。これに対処するために、テストを分離しようとします。上記の例では、すべてのテストで新しいAuthorが作成され、それに関連する他のすべてのエンティティが作成されるため、衝突はまれです。破損する可能性があるが、DBレベルの制約として表現できない残りの不変条件を処理するために、すべての単一テストの後に実行される可能性のあるエラー状態のプログラムチェックを使用します(そして、それらはCIで実行されますが、通常はパフォーマンスのためにローカルでオフにされます理由)。

3
Roman Konoval

JDBCベースのプロジェクト(直接的または間接的に、たとえばJPA、EJBなど)の場合は、データベース全体ではなくモックアップを作成できます(そのような場合は、実際のRDBMSでテストデータベースを使用した方がよい)。 。

利点は、JDBCデータ(結果セット、更新カウント、警告など)がバックエンドと同じであるため、その方法に伴う抽象化です:prod db、テストdb、または各テストに提供されるモックアップデータのみ場合。

各ケースでモックアップされたJDBC接続では、テストデータベースを管理する必要はありません(クリーンアップ、一度に1つのテストのみ、フィクスチャの再読み込みなど)。すべてのモックアップ接続は分離されており、クリーンアップする必要はありません。 JDBC交換をモックアップするために、各テストケースに必要な最小限のフィクスチャのみが提供されます。これにより、テストデータベース全体の管理の複雑さを回避できます。

Acolyteは、この種のモックアップ用のJDBCドライバーとユーティリティを含む私のフレームワークです。 http://acolyte.eu.org .

2
cchantep