運用データベース システムを使用しないテスト

このページでは、お使いのデータベースをテスト ダブルに交換することで、運用環境で動作するアプリケーションに対してデータベース システムを使用しない自動テストを作成する手法について説明します。 この方法にはさまざまなテスト ダブルとアプローチがあります。さまざまなオプションを完全に理解するには、「テスト戦略の選択」をよく読むことをお勧めします。 最後に、運用データベース システムに対してテストすることもできます。この点については、「運用データベース システムに対するテスト」を参照してください。

ヒント

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

リポジトリ パターン

運用データベース システムを使用しないテストを作成することにした場合、その推奨される手法はリポジトリ パターンです。この詳細な背景については、このセクションを参照してください。 リポジトリ パターンを実装する最初の手順は、EF Core LINQ クエリを別のレイヤーに抽出することです。後でこれを使ってスタブまたはモックにすることができます。 ブログ システムのリポジトリ インターフェイスの例を次に示します。

public interface IBloggingRepository
{
    Blog GetBlogByName(string name);

    IEnumerable<Blog> GetAllBlogs();

    void AddBlog(Blog blog);

    void SaveChanges();
}

運用環境用の部分的な実装例を次に示します。

public class BloggingRepository : IBloggingRepository
{
    private readonly BloggingContext _context;

    public BloggingRepository(BloggingContext context)
        => _context = context;

    public Blog GetBlogByName(string name)
        => _context.Blogs.FirstOrDefault(b => b.Name == name);

    // Other code...
}

それほど多くの処理はありません。リポジトリは単に EF Core コンテキストをラップし、データベースのクエリと更新を実行するメソッドを公開しています。 注目すべき重要な点は、GetAllBlogs メソッドは IEnumerable<Blog> を返し、IQueryable<Blog> を返さないことです。 後者を返すことは、結果に対してクエリ演算子が構成される可能性があり、クエリの変換には EF Core が関与する必要があることを意味します。そのため、そもそもリポジトリを持つという目的が失われます。 IEnumerable<Blog> を使うと、リポジトリが返すものを簡単にスタブまたはモックにすることができます。

ASP.NET Core アプリケーションの場合、アプリケーションの ConfigureServices に以下を追加することで、依存関係の挿入のサービスとしてリポジトリを登録する必要があります。

services.AddScoped<IBloggingRepository, BloggingRepository>();

最後に、コントローラーに EF Core コンテキストではなく、リポジトリ サービスが挿入され、それに対してメソッドを実行します。

private readonly IBloggingRepository _repository;

public BloggingControllerWithRepository(IBloggingRepository repository)
    => _repository = repository;

[HttpGet]
public Blog GetBlog(string name)
    => _repository.GetBlogByName(name);

この時点で、アプリケーションはリポジトリ パターンに従って設計されています。データ アクセス層 (EF Core) との唯一の接点は、アプリケーション コードと実際のデータベース クエリの間の仲介役として機能するリポジトリ層を経由するようになりました。 これで、単にリポジトリをスタブにするか、お気に入りのモック ライブラリを使ってモックにすることで簡単にテストを作成できます。 一般的な Moq ライブラリを使ったモックベースのテストの例を次に示します。

[Fact]
public void GetBlog()
{
    // Arrange
    var repositoryMock = new Mock<IBloggingRepository>();
    repositoryMock
        .Setup(r => r.GetBlogByName("Blog2"))
        .Returns(new Blog { Name = "Blog2", Url = "http://blog2.com" });

    var controller = new BloggingControllerWithRepository(repositoryMock.Object);

    // Act
    var blog = controller.GetBlog("Blog2");

    // Assert
    repositoryMock.Verify(r => r.GetBlogByName("Blog2"));
    Assert.Equal("http://blog2.com", blog.Url);
}

完全なサンプル コードはここで確認できます。

SQLite インメモリ

SQLite は、運用データベース システム (例: 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 }));