次の方法で共有


実稼働データベース システムを使用しないテスト

このページでは、アプリケーションが運用環境で実行されるデータベース システムを含まない自動テストを作成する手法について説明します。この手法では、データベースを テストダブルと交換します。 これにはさまざまな種類のテスト ダブルとアプローチがあり、さまざまなオプションを完全に理解するために、テスト戦略の選択 を十分に読んでおくことをお勧めします。 最後に、運用データベース システムに対してテストすることもできます。これは、実稼働データベース システムに対する テストで説明されています。

ヒント

このページ xUnit 手法を示しますが、NUnitを含む他のテスト フレームワークにも同様の概念 存在します。

リポジトリ パターン

実稼働データベース システムを使用せずにテストを記述することにした場合は、リポジトリ パターンを使用することをお勧めします。この背景の詳細については、このセクション参照してください。 リポジトリ パターンを実装する最初の手順は、EF Core LINQ クエリを別のレイヤーに抽出することです。後でスタブまたはモックします。 ブログ システムのリポジトリ インターフェイスの例を次に示します。

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 のメモリ内テストの完全なサンプル コードについては、こちら 参照してください。

メモリ内プロバイダー

テストの概要ページで説明したように、インメモリ プロバイダーをテストに使用することは強くお勧めしません。の代わりに 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 }));