データベース駆動型アプリケーションの単体テストに最適な戦略は何ですか?
-
02-07-2019 - |
質問
私は、バックエンドでさまざまな複雑さのデータベースによって駆動される多くのWebアプリケーションを使用しています。通常、ビジネスおよびプレゼンテーションロジックとは別の ORM レイヤーがあります。これにより、ビジネスロジックの単体テストが非常に簡単になります。個別のモジュールに実装することができ、テストに必要なデータはオブジェクトのモッキングを通じて偽造できます。
しかし、ORMとデータベース自体のテストには常に問題と妥協が伴います。
長年にわたり、いくつかの戦略を試しましたが、どれも完全に満足するものではありませんでした。
-
既知のデータでテストデータベースをロードします。 ORMに対してテストを実行し、正しいデータが返されることを確認します。ここでの欠点は、テストデータベースがアプリケーションデータベースのスキーマの変更に対応しなければならず、同期が取れなくなる可能性があることです。また、人工データに依存しており、愚かなユーザー入力のために発生するバグを公開しない場合があります。最後に、テストデータベースが小さい場合、インデックスがないなどの非効率性は明らかになりません。 (OK、最後の1つは実際に単体テストを使用するべきものではありませんが、害はありません。)
-
本番データベースのコピーをロードし、それに対してテストします。ここでの問題は、いつでも本番DBに何があるかわからない場合があることです。データが時間とともに変化する場合、テストを書き直す必要がある場合があります。
これらの戦略は両方とも特定のデータに依存しており、単体テストでは機能のみをテストする必要があると指摘する人もいます。そのために、私は提案を見ました:
- 模擬データベースサーバーを使用し、特定のメソッド呼び出しに応答してORMが正しいクエリを送信していることのみを確認します。
もしあれば、データベース駆動型アプリケーションのテストにどの戦略を使用しましたか?あなたに最適なものは何ですか?
解決
実際に最初のアプローチを使用してかなり成功しましたが、少し異なる方法であなたの問題のいくつかを解決すると思います:
-
チェックアウト後に誰でも現在のデータベーススキーマを作成できるように、スキーマとスクリプトをソース管理で作成するためにスキーマ全体を保持します。さらに、ビルドプロセスの一部によって読み込まれるデータファイルにサンプルデータを保持します。エラーの原因となるデータを発見したら、サンプルデータに追加して、エラーが再発しないことを確認します。
-
継続的統合サーバーを使用して、データベーススキーマを構築し、サンプルデータをロードし、テストを実行します。これが、テストデータベースの同期を維持する方法です(テスト実行ごとに再構築します)。これには、CIサーバーが独自の専用データベースインスタンスのアクセス権と所有権を持っている必要がありますが、dbスキーマを1日に3回構築すると、おそらく配信の直前まで見つからなかったエラーを劇的に見つけるのに役立ちました(遅くない場合) )。すべてのコミットの前にスキーマを再構築するとは言えません。誰か?このアプローチを使用する必要はありません(多分そうすべきですが、誰かが忘れたとしても大したことではありません)。
-
私のグループでは、ユーザー入力は(dbではなく)アプリケーションレベルで行われるため、これは標準の単体テストでテストされます。
プロダクションデータベースコピーのロード:
これは私の最後の仕事で使用されたアプローチでした。それはいくつかの問題の大きな痛みの原因でした:
- 製品版のコピーは古くなっています
- コピーのスキーマに変更が加えられ、本番システムに反映されません。この時点で、スキーマは異なります。面白くない。
データベースサーバーのモック:
これは現在の仕事でも行っています。コミットするたびに、模擬DBアクセサーが挿入されたアプリケーションコードに対してユニットテストを実行します。その後、1日に3回、上記の完全なdbビルドを実行します。両方のアプローチをお勧めします。
他のヒント
これらの理由により、インメモリDB(HSQLDBまたはDerby)に対して常にテストを実行しています:
- テストDBに保持するデータとその理由を考えることができます。実稼働DBをテストシステムに持ち込むだけで、「何をしているのか、なぜ、そして何かが壊れても自分ではない!!」 ;)
- データベースを新しい場所でわずかな労力で再作成できるようにします(たとえば、本番環境からバグを複製する必要がある場合)
- DDLファイルの品質に非常に役立ちます。
テストが開始されるとインメモリDBに新しいデータがロードされ、ほとんどのテストの後、ROLLBACKを呼び出して安定させます。 常にテストDBのデータを安定させます!データが常に変化する場合、テストすることはできません。
データは、SQL、テンプレートDB、またはダンプ/バックアップからロードされます。 VCSに配置できるため、読み取り可能な形式のダンプが好きです。それでもうまくいかない場合は、CSVファイルまたはXMLを使用します。膨大な量のデータをロードする必要がある場合は...しません。膨大な量のデータを読み込む必要はありません:)単体テスト用ではありません。パフォーマンステストは別の問題であり、異なるルールが適用されます。
私は長い間この質問をしてきましたが、それに対する特効薬はないと思います。
現在私がしていることは、DAOオブジェクトのモックを作成し、データベースに存在する可能性のあるデータの興味深いケースを表すオブジェクトの優れたコレクションのメモリ表現を保持することです。
このアプローチで見られる主な問題は、DAOレイヤーと相互作用するコードのみをカバーしているが、DAO自体をテストしないことであり、私の経験では、そのレイヤーで多くのエラーが発生することがわかりますまあ。また、データベースに対して実行するユニットテストをいくつか保持します(TDDまたはローカルでのクイックテストを使用するため)。これらのテストは、継続的な統合サーバーで実行されることはありません。 CIサーバーで実行されるテストは自己完結型である必要があると思います。
別のアプローチは非常に興味深いと思いますが、少し時間を要するので常に価値があるとは限りませんが、単体テスト内で実行するだけの組み込みデータベースで実稼働に使用するのと同じスキーマを作成することです。
このアプローチがカバレッジを改善することは間違いありませんが、現在のDBMSと組み込みの置換の両方で動作させるにはANSI SQLに可能な限り近づけなければならないため、いくつかの欠点があります。
コードとの関連性が高いと思われる場合でも、 DbUnit 。
何らかの方法でデータベースをモックできるツールがあっても(例: jOOQ ' s MockConnection
この回答で見ることができるa>-免責事項、私はjOOQのベンダーで働いています)、私はに助言します複雑なクエリで大規模なデータベースをモックすることはできません。
ORMの統合テストだけを行う場合でも、ORMがデータベースに対して非常に複雑な一連のクエリを発行することに注意してください。
- 構文
- 複雑さ
- 注文(!)
実際にモック内に小さなデータベースを構築し、送信されたSQLステートメントを解釈する場合を除き、賢明なダミーデータを生成するためにすべてをモックすることは非常に困難です。そうは言っても、よく知られたデータで簡単にリセットできる、よく知られた統合テストデータベースを使用します。これに対して統合テストを実行できます。
最初のものを使用します(テストデータベースに対してコードを実行します)。このアプローチで提起する唯一の実質的な問題は、スキーマが同期しなくなる可能性です。これは、データベースにバージョン番号を保持し、各バージョンの増分の変更を適用するスクリプトを介してすべてのスキーマを変更することで対処します。
また、最初にテスト環境に対してすべての変更(データベーススキーマを含む)を行うため、最終的には逆になります。すべてのテストに合格したら、スキーマの更新を運用ホストに適用します。また、開発システムにテストデータベースとアプリケーションデータベースを別々にペアにしておくと、実際の運用環境に触れる前に、dbのアップグレードが適切に機能することを確認できます。
私は最初のアプローチを使用していますが、あなたが言及した問題に対処できるように少し異なります。
DAOのテストを実行するために必要なものはすべてソース管理にあります。 DBを作成するためのスキーマとスクリプトが含まれています(これには、dockerが非常に適しています)。埋め込みDBを使用できる場合-速度のために使用します。
説明されている他のアプローチとの重要な違いは、テストに必要なデータがSQLスクリプトまたはXMLファイルから読み込まれないことです。すべて(実質的に一定である一部の辞書データを除く)は、ユーティリティ関数/クラスを使用してアプリケーションによって作成されます。
主な目的は、テストで使用されるデータを作成することです
- テストに非常に近い
- 明示的(データにSQLファイルを使用すると、どのデータがどのテストで使用されているかを確認するのが非常に困難になります)
- テストを無関係な変更から分離します。
基本的に、これらのユーティリティでは、テスト自体にテストに不可欠なものだけを宣言的に指定し、無関係なものを省略することができます。
実際の意味を理解するために、 Comments
から Author
によって書かれた Post
で動作する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ファイルに比べていくつかの利点があります。
- コードのメンテナンスがはるかに簡単です(たとえば、Authorなどの多くのテストで参照されるエンティティに必須の列を追加すると、多くのファイル/レコードを変更する必要はなく、ビルダーやファクトリーを変更するだけです)
- 特定のテストに必要なデータは、テスト自体に記述されており、他のファイルには記述されていません。この近接性は、テストのわかりやすさにとって非常に重要です。
ロールバックとコミット
テストの実行時にコミットする方が便利だと思います。まず、コミットが発生しない場合、一部の効果(たとえば、 DEFERRED CONSTRAINTS
)をチェックできません。次に、テストが失敗した場合、データはロールバックによって元に戻されないため、DBで検査できます。
これには、テストで破損したデータが生成される可能性があり、他のテストでエラーが発生するという欠点があります。これに対処するために、テストを分離しようとします。上記の例では、すべてのテストで新しい Author
が作成され、それに関連する他のすべてのエンティティが作成されるため、衝突はまれです。破損する可能性があるが、DBレベルの制約として表現できない残りの不変条件を処理するために、すべての単一テストの後に実行される可能性のある誤った条件に対していくつかのプログラムによるチェックを使用します理由)。
JDBCベースのプロジェクト(直接的または間接的に、たとえばJPA、EJBなど)の場合、データベース全体ではなくモックアップできます(そのような場合は、実際のRDBMSでテストデータベースを使用した方が良いでしょう)が、モックアップのみJDBCレベル。
利点は、JDBCデータ(結果セット、更新カウント、警告、...)がバックエンドと同じであるため、その方法に伴う抽象化です:prod db、test db、または提供された一部のモックアップデータテストケースごとに。
JDBC接続が各ケースでモックアップされているため、テストデータベースを管理する必要はありません(クリーンアップ、一度に1つのテストのみ、フィクスチャの再読み込みなど)。すべてのモックアップ接続は分離されており、クリーンアップする必要はありません。 JDBC交換をモックアップするために、各テストケースに最小限必要なフィクスチャのみが提供されます。これにより、テストデータベース全体の管理の複雑さを回避できます。
Acolyteは、この種のモックアップ用のJDBCドライバーとユーティリティを含む私のフレームワークです: http://acolyte.eu。 org 。