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


346

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

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

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

  • 既知のデータを含むテストデータベースを読み込みます。ORMに対してテストを実行し、適切なデータが返されることを確認します。ここでの不利な点は、テストDBがアプリケーションデータベースのスキーマの変更に対応しなければならず、同期が取れなくなる可能性があることです。また、人為的なデータに依存しており、愚かなユーザー入力が原因で発生するバグを公開しない場合があります。最後に、テストデータベースが小さい場合、インデックスの欠落などの非効率性は明らかになりません。(最後の1つは、実際に単体テストを使用する目的ではありませんが、問題はありません。)

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

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

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

データベース駆動型アプリケーションをテストする場合、どのような戦略を使用しましたか(ある場合)。あなたにとって何が一番効果的でしたか?


一意のインデックスなどの場合は、テスト環境にデータベースインデックスがまだあるはずです。
dtc

私はここで個人的にこの質問を気にしませんが、ルールに従っていれば、この質問は、stackoverflowではなく、softwareengineering.stackexchange Webサイトに関するものです。
ITExpert

回答:


155

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

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

  2. 継続的インテグレーションサーバーを使用して、データベーススキーマを構築し、サンプルデータを読み込み、テストを実行します。これは、テストデータベースの同期を保つ方法です(テスト実行ごとにデータベースを再構築します)。これには、CIサーバーが専用のデータベースインスタンスにアクセスして所有権を持っている必要がありますが、1日3回構築されたdbスキーマを使用することで、配信の直前までは検出されなかったエラーを見つけるのに非常に役立ちました)。すべてのコミットの前にスキーマを再構築するとは言えません。誰か?このアプローチでは、あなたはする必要はありません(たぶん私たちはそうすべきですが、誰かが忘れても大したことではありません)。

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

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

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

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


37
本番データベースのコピーをロードすると、セキュリティとプライバシーにも影響します。それが大きくなると、そのコピーを取り、開発環境に置くことは大きな問題になる可能性があります。
WW。

正直なところ、これは大きな痛みです。私はテストするのが初めてで、テストしたいオームも書いた。私はすでに最初の方法を使用しましたが、それはテストユニットを作成しないことを読みました。私は特定のdbエンジン機能を使用しているため、DAOをモックするのは困難です。それが機能し、他の人が使用しているので、私の現在の方法を使用するのは悪いと思います。自動化されたテストは、まあまあです。ありがとう。
霜降りの素晴らしい

2
私は2つの異なる大規模プロジェクトを管理していますが、その1つではこのアプローチは完璧でしたが、もう1つのプロジェクトでこれを実装しようとすると多くの問題が発生しました。したがって、テストを実行するために毎回スキーマを簡単に再作成できるかどうかに応じて、私は現在、この最後の問題の新しい解決策を見つけることに取り組んでいます。
クロス

2
この場合、ラウンドハウスのようなデータベースのバージョン管理ツールを使用することは間違いなく価値があります-移行を実行できるものです。これは任意のDBインスタンスで実行でき、スキーマが最新であることを確認する必要があります。さらに、移行スクリプトを作成するときは、テストデータも作成して、移行とデータの同期を保つ必要があります。
jedd.ahyoung 2016年

サルのパッチとモックをより適切に使用し、書き込み操作を回避する
Nickpick

56

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

  • これにより、テストDBに保持するデータとその理由を考えることができます。プロダクションDBをテストシステムに持ち込むだけで、「何をしているのか、なぜなのか、何かが壊れたのは私ではなかったのです!!」;)
  • 新しい場所でデータベースをほとんど手間をかけずに再作成できるようにします(たとえば、本番環境からバグを複製する必要がある場合)。
  • DDLファイルの品質を大幅に向上させます。

テストが開始すると、インメモリDBに新しいデータが読み込まれ、ほとんどのテストの後で、ROLLBACKを呼び出して安定性を保ちます。常にテストDBのデータを安定させてください。データが常に変化している場合は、テストできません。

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


1
速度が(特に)インメモリDBを使用する唯一の理由ですか?
rinogo 2014年

2
もう1つの利点は、その「使い捨て」の性質かもしれません。インメモリDBを強制終了するだけです。(ただし、これまでに述べたROLLBACKアプローチなど、これを実現する他の方法があります)
rinogo '31 / 01/31

1
利点は、各テストで戦略を個別に選択できることです。子スレッドで作業を行うテストがあります。つまり、Springは常にデータをコミットします。
アーロンディグラ2014

@アーロン:私たちもこの戦略に従っています。インメモリモデルが実際のデータベースと同じ構造であると断言するための戦略は何ですか?
ギヨーム

1
@ギラウム:同じSQLファイルからすべてのデータベースを作成しています。H2は、主要なデータベースのSQL特異性のほとんどをサポートしているため、これに最適です。それが機能しない場合は、元のSQLを取得してそれをインメモリデータベースのSQLに変換するフィルターを使用します。
アーロンディグラ

14

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

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

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

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

このアプローチでカバレッジが向上することは間違いありませんが、現在のDBMSと埋め込み置換の両方で機能させるにはANSI SQLにできるだけ近づける必要があるため、いくつかの欠点があります。

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


13

あなたは一つの方法または別の(例えばでデータベースを模擬できるようにするツールがある場合でもjOOQさんMockConnection、で見ることができるこの回答 -免責事項、jOOQのベンダーのための私の仕事は)、私は助言ではない複雑で大規模データベースを模擬しますクエリ。

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

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

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


5

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

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


3

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

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

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

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

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

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

実際の意味を理解するために、によって記述されたCommentsからPosts で機能するDAOのテストを考えてみましょうAuthors。このような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で実行されますが、通常はパフォーマンスのためにローカルでオフにされます理由)。


SQLスクリプトの代わりにエンティティとormを使用してデータベースをシードする場合、モデルに変更を加えた場合にコンパイラーがシードコードを修正するように強制するという利点もあります。もちろん、静的型付け言語を使用する場合にのみ関連します。
daramasala

明確にするために、アプリケーション全体でユーティリティ関数/クラスを使用していますか、それともテストだけですか?
エラ

@Ellaこれらのユーティリティ関数は通常、テストコード以外では必要ありません。たとえばについて考えてみてくださいPostBuilder.post()。投稿のすべての必須属性にいくつかの値を生成します。これは量産コードでは必要ありません。
ローマンコノヴァル

2

JDBCベースのプロジェクト(直接または間接的に、たとえばJPA、EJBなど)の場合、データベース全体をモックアップすることはできません(その場合、実際のRDBMSでテストデータベースを使用する方が適切です)が、JDBCレベルでのみモックアップできます。 。

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

いずれの場合もJDBC接続がモックアップされているため、テストデータベースを管理する必要はありません(クリーンアップ、一度に1つのテストのみ、フィクスチャのリロードなど)。すべてのモックアップ接続が分離され、クリーンアップする必要はありません。各テストケースでは、JDBC交換を模擬するために最小限必要なフィクスチャのみが提供され、テストデータベース全体の管理の複雑さを回避するのに役立ちます。

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

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