実稼働データベース システムを使用しないテスト
このページでは、アプリケーションが運用環境で実行されるデータベース システムを含まない自動テストを作成する手法について説明します。この手法では、データベースを テストダブルと交換します。 これにはさまざまな種類のテスト ダブルとアプローチがあり、さまざまなオプションを完全に理解するために、テスト戦略の選択 を十分に読んでおくことをお勧めします。 最後に、運用データベース システムに対してテストすることもできます。これは、実稼働データベース システムに対する
ヒント
このページ
リポジトリ パターン
実稼働データベース システムを使用せずにテストを記述することにした場合は、リポジトリ パターンを使用することをお勧めします。この背景の詳細については、このセクション
public interface IBloggingRepository
{
Task<Blog> GetBlogByNameAsync(string name);
IAsyncEnumerable<Blog> GetAllBlogsAsync();
void AddBlog(Blog blog);
Task SaveChangesAsync();
}
...運用環境で使用するための部分的なサンプル実装を次に示します。
public class BloggingRepository : IBloggingRepository
{
private readonly BloggingContext _context;
public BloggingRepository(BloggingContext context)
=> _context = context;
public async Task<Blog> GetBlogByNameAsync(string name)
=> await _context.Blogs.FirstOrDefaultAsync(b => b.Name == name);
// Other code...
}
それほど多くはありません。リポジトリは単に EF Core コンテキストをラップし、データベース クエリと更新を実行するメソッドを公開します。 注意すべき重要な点は、GetAllBlogs
メソッドは IQueryable<Blog>
ではなく IAsyncEnumerable<Blog>
(または IEnumerable<Blog>
) を返すということです。 後者を返すと、クエリ演算子を結果に対して構成できるため、EF Core がクエリの変換にまだ関与している必要があります。これは、最初にリポジトリを持つという目的を打ち負かすでしょう。 IAsyncEnumerable<Blog>
を使用すると、リポジトリから返されるものを簡単にスタブまたはモックすることができます。
ASP.NET Core アプリケーションの場合は、アプリケーションの ConfigureServices
に次を追加して、依存関係の挿入でリポジトリをサービスとして登録する必要があります。
services.AddScoped<IBloggingRepository, BloggingRepository>();
最後に、コントローラーは EF Core コンテキストではなくリポジトリ サービスに挿入され、それに対してメソッドを実行します。
private readonly IBloggingRepository _repository;
public BloggingControllerWithRepository(IBloggingRepository repository)
=> _repository = repository;
[HttpGet]
public async Task<Blog> GetBlog(string name)
=> await _repository.GetBlogByNameAsync(name);
この時点で、アプリケーションはリポジトリ パターンに従って設計されています。データ アクセス層 (EF Core) との唯一の接触点は、アプリケーション コードと実際のデータベース クエリの間のメディエーターとして機能するリポジトリ レイヤーを介して行われます。 テストは、リポジトリをスタブアウトしたり、お気に入りのモックライブラリを使用してモックしたりすることで、簡単に記述できるようになりました。 一般的な Moq ライブラリを使用したモックベースのテストの例を次に示します。
[Fact]
public async Task GetBlog()
{
// Arrange
var repositoryMock = new Mock<IBloggingRepository>();
repositoryMock
.Setup(r => r.GetBlogByNameAsync("Blog2"))
.Returns(Task.FromResult(new Blog { Name = "Blog2", Url = "http://blog2.com" }));
var controller = new BloggingControllerWithRepository(repositoryMock.Object);
// Act
var blog = await controller.GetBlog("Blog2");
// Assert
repositoryMock.Verify(r => r.GetBlogByNameAsync("Blog2"));
Assert.Equal("http://blog2.com", blog.Url);
}
完全なサンプル コードは、ここで
SQLite インメモリ
SQLite は、運用データベース システム (e.g. SQL Server) ではなく、テスト スイートの EF Core プロバイダーとして簡単に構成できます。詳細については、SQLite プロバイダーのドキュメント を参照してください。 ただし、テスト時には SQLite の インメモリ データベース 機能を使用することをお勧めします。テスト間の分離が容易であり、実際の SQLite ファイルを処理する必要がないためです。
メモリ内 SQLite を使用するには、低レベルの接続が開かれるたびに新しいデータベースが作成され、その接続が閉じられたときに削除されることを理解することが重要です。 通常の使用では、EF Core の DbContext
は、必要に応じて、クエリが実行されるたびにデータベース接続を開いて閉じ、不必要に長い時間接続を維持しないようにします。 ただし、メモリ内 SQLite を使用すると、毎回データベースがリセットされます。回避策として、EF Core に渡す前に接続を開き、テストが完了したときにのみ閉じるように調整します。
public SqliteInMemoryBloggingControllerTest()
{
// Create and open a connection. This creates the SQLite in-memory database, which will persist until the connection is closed
// at the end of the test (see Dispose below).
_connection = new SqliteConnection("Filename=:memory:");
_connection.Open();
// These options will be used by the context instances in this test suite, including the connection opened above.
_contextOptions = new DbContextOptionsBuilder<BloggingContext>()
.UseSqlite(_connection)
.Options;
// Create the schema and seed some data
using var context = new BloggingContext(_contextOptions);
if (context.Database.EnsureCreated())
{
using var viewCommand = context.Database.GetDbConnection().CreateCommand();
viewCommand.CommandText = @"
CREATE VIEW AllResources AS
SELECT Url
FROM Blogs;";
viewCommand.ExecuteNonQuery();
}
context.AddRange(
new Blog { Name = "Blog1", Url = "http://blog1.com" },
new Blog { Name = "Blog2", Url = "http://blog2.com" });
context.SaveChanges();
}
BloggingContext CreateContext() => new BloggingContext(_contextOptions);
public void Dispose() => _connection.Dispose();
テストで CreateContext
を呼び出すようになり、コンストラクターで設定した接続を使用してコンテキストが返され、シードされたデータを含むクリーンなデータベースが確保されます。
SQLite のメモリ内テストの完全なサンプル コードについては、こちら
メモリ内プロバイダー
public InMemoryBloggingControllerTest()
{
_contextOptions = new DbContextOptionsBuilder<BloggingContext>()
.UseInMemoryDatabase("BloggingControllerTest")
.ConfigureWarnings(b => b.Ignore(InMemoryEventId.TransactionIgnoredWarning))
.Options;
using var context = new BloggingContext(_contextOptions);
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
context.AddRange(
new Blog { Name = "Blog1", Url = "http://blog1.com" },
new Blog { Name = "Blog2", Url = "http://blog2.com" });
context.SaveChanges();
}
メモリ内テストの完全なサンプル コードは、
メモリ内データベースの名前付け
メモリ内データベースは単純な文字列名で識別され、同じ名前を指定して同じデータベースに複数回接続できます (このため、上記のサンプルでは各テストの前に EnsureDeleted
を呼び出す必要があります)。 ただし、メモリ内データベースはコンテキストの内部サービス プロバイダーにルート化されていることに注意してください。ほとんどの場合、コンテキストは同じサービス プロバイダーを共有しますが、さまざまなオプションを使用してコンテキストを構成すると、新しい内部サービス プロバイダーの使用がトリガーされる可能性があります。 その場合は、メモリ内データベースを共有するすべてのコンテキストに対して、InMemoryDatabaseRoot の同じインスタンスを UseInMemoryDatabase
に明示的に渡します (これは通常、静的な InMemoryDatabaseRoot
フィールドを持つことによって行われます)。
トランザクション
既定では、トランザクションが開始されると、メモリ内プロバイダはトランザクションがサポートされていないため例外を発生させます。 上記のサンプルのように InMemoryEventId.TransactionIgnoredWarning
を無視するように EF Core を構成することで、代わりにトランザクションを自動的に無視することもできます。 ただし、コードがトランザクション セマンティクスに実際に依存している場合 (たとえば、変更をロールバックするロールバックに依存する場合など)、テストは機能しません。
ビュー
メモリ内プロバイダーでは、次の ToInMemoryQueryを使用して、LINQ クエリを使用してビューを定義できます。
modelBuilder.Entity<UrlResource>()
.ToInMemoryQuery(() => context.Blogs.Select(b => new UrlResource { Url = b.Url }));
.NET