テスト戦略の選択
概要に関するページに示すように、1 つ基本的な決定を行う必要があります。それは、アプリケーションの場合と同様にテストに運用データベースシステムを含めるかどうか、あるいは運用データベース システムをテスト ダブルに置き換えてそれに対してテストを実施するかどうかということです。
テスト ダブルに置き換えるのではなく、実際の外部リソースに対して行うテストには、次のような問題が発生する可能性があります。
- 多くの場合、実際の外部リソースに対してテストを行うことは、明らかに不可能であり、また実用的ではありません。 たとえば、ご利用のアプリケーションは、(レート制限がある、またはテスト環境が不足しているという理由で) 簡単にテストできないサービスとやり取りする場合があります。
- 実際の外部リソースを含めることが可能な場合でも、非常に遅くなる可能性があります。たとえば、クラウド サービスに対して大量のテストを実行すると、テストに時間がかかりすぎる可能性があります。 テストは開発者の日常的なワークフローの一部であるため、テストは迅速に実行されることが重要です。
- 外部リソースに対してテストを実行すると、テストが互いに干渉するという、分離の問題を発生する可能性があります。 たとえば、データベースに対して複数のテストを並行して実行すると、データが変更され、さまざまな理由で互いに失敗する可能性があります。 テスト ダブルを使用すると、これを回避できます。各テストが独自のインメモリ リソースに対して実行されるため、他のテストから必然的に分離されるからです。
ただし、テスト ダブルに対して行ったテストが合格したからといって、実際の外部リソースに対してプログラムを実行したときに確実に動作するとは限りません。 たとえば、データベースのテスト ダブルでは大文字と小文字が区別される文字列比較を実行するのに対し、運用データベース システムでは大文字と小文字が区別されない比較を実行する場合があります。 このような問題は、実際の運用データベースに対してテストが実行され場合にしか明らかにならないため、これらのテストはテスト戦略における重要な部分となります。
データベースに対するテストは思ったよりも簡単かもしれない
実際のデータベースに対するテストは上記の問題点を伴うため、開発者はテスト ダブルを最初に使用すること、自身のコンピューター上で頻繁に実行できる堅牢なテスト スイートを用意することをよく求められます。これに対し、データベースを必要とするテストは実行頻度がはるかに低く、多くの場合、カバレッジもはるかに低くなります。 後者についてもっとよく考えることをお勧めします。そして、データベースは実際には、人々が考えがちであるよりもはるかに上記の問題の影響を受ける可能性が低いことを提言しておきます。
- ほとんどのデータベースは、今日、開発者のコンピューターに簡単にインストールできます。 Docker などのコンテナーベースのテクノロジを使用すると、これを非常に簡単に行うことができます。Github Workspaces や Dev Container などのテクノロジによって開発環境全体 (データベースを含む) が自動的に設定されます。 SQL Server を使用する場合は、Windows 上で LocalDB に対してテストすることも、Linux 上で Docker イメージを簡単に設定することもできます。
- ローカル データベースに対するテスト (妥当なテスト データセットを使用した) は、通常は非常に高速です。通信は完全にローカルで行われ、テスト データは、通常、データベース側のメモリにバッファーされます。 EF Core 自体には、SQL Server のみを対象にした 30,000 以上のテストが含まれています。これらは数分で確実に完了し、1 回のコミットごとに CI で実行され、ローカルで開発者により、かなり頻繁に実行されます。 一部の開発者は、速度の面で必要であると信じて、インメモリ データベース ("フェイク") に目を向けますが、実際にはほとんどの場合がそうではありません。
- 実際のデータベースに対してテストを実行する場合、テストによってデータが変更され、テストが相互に干渉する可能性があるため、分離は実際には困難です。 ただし、データベース テスト シナリオでは、分離を実現する手法が各種あります。これらについては、「運用データベース システムに対するテスト」で具体的に説明します。
上記は、テスト ダブルを軽視したり、それらの使用に反対したりすることを意図するものではありません。 1 つには、データベース障害のシミュレーションなど、他の方法でテストできないいくつかのシナリオには、テスト ダブルが必要だからです。 ただし、私たちの経験からすると、ユーザーは上記の理由で、必ずしもそうではない場合でも、遅い、難しい、または信頼できないと思い込んで、データベースに対するテストを躊躇することがよくあります。 「運用データベース システムに対するテスト」は、これに対処することを目的としていて、データベースに対して高速で分離されたテストを記述するためのガイドラインとサンプルを提示しています。
さまざまな種類のテスト ダブル
テスト ダブル とは、大きく異なるさまざまなアプローチを網羅した広義の用語です。 このセクションでは、EF Core アプリケーションをテストする場合にテスト ダブルを必要とする一般的な手法について説明します。
- SQLite (インメモリ モード) をデータベースのフェイクとして使用し、運用データベース システムを置き換えます。
- EF Core インメモリ プロバイダーをデータベースのフェイクとして使用し、運用データベース システムを置き換えます。
DbContext
とDbSet
をモック化またはスタブ化します。- EF Core とアプリケーション コードの間にリポジトリ レイヤーを導入し、そのレイヤーをモック化またはスタブ化します。
以下では、各メソッドが何を意図するものなのかを調べ、他の手法と比較します。 さまざまなメソッドの説明を読んで、それぞれについて理解を深めることをお勧めします。 運用データベース システムを含めないテストを作成することにした場合は、リポジトリ レイヤーが、データ レイヤーの包括的で信頼性の高いスタブ化やモック化を可能にする唯一のアプローチです。 ただし、このアプローチは実装とメンテナンスの点で大きなコストがかかります。
データベースのフェイクとしての SQLite
可能なテスト アプローチの 1 つは、運用データベース (SQL Server など) を SQLite と交換し、それをテスト用の "フェイク" として効果的に使用することです。 セットアップが容易であることの他に、SQLite には、テストに特に役立つインメモリ データベース機能があります。各テストは、固有のインメモリ データベースで自然に分離され、実際のファイルを管理する必要はありません。
ただし、これを行う場合、EF Core では、各種のデータベース プロバイダーの動作がそれぞれ異なることを事前に理解しておくことが重要です。EF Core では、基になるデータベース システムのすべての側面について抽象化を試みるわけではありません。 つまり、基本的に、SQLite に対するテストの結果は、SQL Server やその他のデータベースに対する場合と必ずしも同じになるわけではありません。 考えられる動作の相違点の例を次に示します。
- 同じ LINQ クエリであっても、プロバイダーが異なると、異なる結果が返される場合があります。 たとえば、SQL Server では既定で大文字と小文字が区別されない文字列比較が行われますが、SQLite では大文字と小文字が区別されます。 このため、テストが SQLite に対して成功し、SQL Server に対しては失敗する可能性があります (またはその逆も同様です)。
- SQL Server で動作するクエリの中には、SQLite ではサポートされていないものがあります。この 2 つのデータベースでの SQL サポートが厳密には異なるからです。
- ご利用のクエリで SQL Server の
EF.Functions.DateDiffDay
など、プロバイダー固有のメソッドがたまたま使用された場合、そのクエリは SQLite では失敗し、テストできません。 - 厳密に実行されている内容に応じて、生 SQL が機能したり、失敗したり、異なる結果が返されたりする場合があります。 SQL 言語には、データベース間でさまざまな相違点があります。
運用データベース システムに対してテストを実行する場合と比較して、SQLite を使い始めるのは比較的簡単であり、多くのユーザーがそうしています。 残念ながら、上記の制限は、最初はそうではないように見えても、EF Core アプリケーションをテストすると、最終的に問題になる傾向があります。 そのため、実際のデータベースに対してテストを作成すること、あるいはテスト ダブルの使用が絶対に必要なのであれば以下で説明するようにリポジトリ パターンのコストを考慮すること、をお勧めします。
テストに SQLite を使用する方法については、こちらのセクションを参照してください。
データベースのフェイクとしてのインメモリ
SQLite の代替えとして、EF Core にはインメモリ プロバイダーも付属しています。 このプロバイダーはもともと EF Core 自体の内部テストをサポートするように設計されましたが、一部の開発者はこれを、EF Core アプリケーションのテストの際にデータベースのフェイクとして使用しています。 これを行うことはお勧めできません: データベースのフェイクであるため、インメモリにも SQLite と同じ問題があります (上記を参照)。さらに、次の追加の制限が課せられます:
- インメモリ プロバイダーは、リレーショナル データベースではないので、一般に SQLite プロバイダーよりもサポートするクエリの種類は少なくなっています。 運用データベースと比較すると、失敗するクエリが多く、また動作が異なるクエリも多くあります。
- トランザクションはサポートされていません。
- 生 SQL は完全にサポートされていません。 これを SQLite と比較してください。SQLite および運用データベース上で SQL が同じように機能する限り、生の SQL を使用することができます。
- インメモリ プロバイダーはパフォーマンスについて最適化されていないため、通常、インメモリ モードでは (また、運用データベース システムでも)、SQLite よりも動作が遅くなります。
要約すると、インメモリには SQLite のすべての欠点に加えて、その他にもいくつかの欠点があります。それと引き換えに得られる利点はありません。 シンプルなインメモリ データベースのフェイクを探している場合は、インメモリ プロバイダーではなく SQLite を使用してください。ただし、以下で説明するように、代わりにリポジトリ パターンの使用を検討してください。
インメモリを使用してテストする方法については、こちらのセクションを参照してください。
DbContext と DbSet のモック化またはスタブ化
このアプローチでは、通常、モック フレームワークを使用して DbContext
と DbSet
のテスト ダブルを作成し、それらのダブルに対してテストを行います。 DbContext
のモック化は、Add や SaveChanges() の呼び出しなど、"クエリ以外" のさまざまな機能をテストする場合に適した方法です。これにより、書き込みシナリオにおいてコードによりそれらが呼び出されたことを確認できます。
ただし、DbSet
"クエリ" 機能を適切にモック化することはできません。クエリは、IQueryable
に対する静的拡張メソッド呼び出しである LINQ 演算子を介して表現されるからです。 したがって、一部の人が "DbSet
のモック化" について話すとき、彼らが本当に意味するのは、シンプルな IEnumerable
と同じように、インメモリ コレクションに基づく DbSet
を作成し、メモリ内のそのコレクションに対してクエリ演算子を評価することです。 これはモックではなく、実際には一種のフェイクです。インメモリ コレクションにより、実際のデータベースが置き換えられます。
DbSet
そのものだけがフェイク化され、クエリはインメモリで評価されるため、このアプローチは最終的に EF Core インメモリ プロバイダーを使用する場合とよく似たものとなります。どちらの手法でも、インメモリ コレクションに対して .NET でクエリ演算子を実行します。 したがって、この手法にも同じ欠点があります: クエリの動作が異なる (大文字と小文字の区別など)、明らかに失敗する (たとえば、プロバイダー固有のメソッドがあるため)、生 SQL が機能しない、トランザクションがかなり無視される。 結果として、通常、クエリ コードをテストする場合は、この手法を避ける必要があります。
リポジトリ パターン
上記のアプローチでは、EF Core の運用データベース プロバイダーをフェイクのテスト プロバイダーと交換すること、またはインメモリ コレクションに基づいて DbSet
を作成することを試みました。 これらの手法は、プログラムの LINQ クエリを引き続き (SQLite で、またはメモリ内で) 評価するという点で類似していて、これが最終的に上で概説した問題の原因となります。特定の運用データベースに対して実行するように設計されたクエリは、他の場所で問題なく確実には、実行できません。
適切で信頼性の高いテスト ダブルを実現するには、アプリケーション コードと EF Core の間を仲介するリポジトリ レイヤーの導入を検討してください。 リポジトリの運用環境の実装は、実際の LINQ クエリを備えていて、EF Core を介してそれらを実行します。 テストでは、実際の LINQ クエリを必要とせずにリポジトリの抽象化が直接スタブ化またはモック化されます。このため、テスト スタックから EF Core を効果的に完全に削除し、アプリケーション コードのみにテストを集中させることができます。
次の図は、データベース フェイク アプローチ (SQLite またはインメモリ) とリポジトリ パターンを比較したものです。
LINQ クエリはテストの一部ではなくなったため、クエリ結果をアプリケーションに直接提供できます。 別の言い方をすれば、前のアプローチでは "クエリ入力" のスタブ化がほぼ可能できますが (たとえば、SQL Server "テーブル" をインメモリ テーブルに置き換えるなど)、実際のクエリ演算子はインメモリで実行されます。 これに対し、リポジトリ パターンを使用すると、"クエリ出力" を直接スタブ化できるため、はるかに強力で集中した単体テストが可能になります。 これを機能させるために、ご利用のリポジトリでは、IQueryable を返すメソッドを公開できないことに注意してください。代わりに IEnumerable を返す必要があります。
ただし、リポジトリ パターンでは、IEnumerable を返すメソッドで各 (テスト可能な) LINQ クエリをカプセル化する必要があるため、ご利用のアプリケーションに追加のアーキテクチャ レイヤーが課され、実装と保守に大きなコストが発生する可能性があります。 このコストは、アプリケーションをテストする方法を選択する際に割り引いてはなりません。特に、リポジトリによって公開されるクエリについて、実際のデータベースに対するテストが引き続き必要になる可能性が高い場合は注意してください。
注目すべき点は、リポジトリにはテスト以外にも利点があることです。 それにより、すべてのデータ アクセス コードがアプリケーション全体に分散するのではなく、1 か所に確実に集中します。そして、アプリケーションで複数のデータベースをサポートする必要がある場合、リポジトリの抽象化が、プロバイダー間でクエリを微調整するのに非常に役立ちます。
リポジトリを使用したテストを示すサンプルについては、こちらのセクションを参照してください。
全体的な比較
次の表は、さまざまなテスト手法の簡単な比較ビューであり、どのアプローチで、どの機能をテストできるかを示しています。
機能 | メモリ内 | SQLite インメモリ | DbContext のモック化 | リポジトリ パターン | データベースに対するテスト |
---|---|---|---|---|---|
テスト ダブルの種類 | フェイク | フェイク | フェイク | モック/スタブ | 実物、ダブルなし |
生 SQL? | いいえ | 依存 | いいえ | イエス | はい |
トランザクション? | いいえ (無視される) | はい | イエス | イエス | はい |
プロバイダー固有の変換? | いいえ | いいえ | 番号 | イエス | はい |
正確なクエリ動作? | 依存 | 依存 | 依存 | はい | はい |
アプリケーション内の任意の場所で LINQ を使用できますか? | はい | イエス | はい | いいえ* | はい |
* スタブ化/モック化するには、すべてのテスト可能なデータベース LINQ クエリを IEnumerable を返すリポジトリ メソッドにカプセル化する必要があります。
まとめ
- 開発者には、実際の運用データベース システムに対して実行されているアプリケーションのテスト カバレッジを十分に確保することをお勧めします。 これにより、アプリケーションが運用環境で実際に動作するという確信が得られ、適切な設計により、テストを確実かつ迅速に実行できます。 これらのテストはいずれの場合も必要になるため、そこから始めて、必要に応じて後でテスト ダブルを使用してテストを追加することをお勧めします。
- テスト ダブルを使用することに決めた場合は、リポジトリ パターンを実装することをお勧めします。これにより、フェイクの EF Core プロバイダー (SQLite/インメモリ) を使用したり、
DbSet
をモック化したりするのではなく、EF Core 上のデータ アクセス層をスタブ化またはモック化することができます。 - 何らかの理由でリポジトリ パターンが実行可能なオプションでない場合は、SQLite インメモリ データベースの使用を検討してください。
- テスト目的でインメモリ プロバイダーを使用することは避けてください。これは推奨されておらず、レガシ アプリケーションでのみサポートされています。
- クエリを実行する目的で
DbSet
をモック化することは避けてください。
.NET